[krita/kazakov/audio-track-support] /: Implement Audio Channel support

Dmitry Kazakov null at kde.org
Mon Dec 26 18:53:34 UTC 2016


Git commit 6419771ee0f77e0b9566381cf90d887124b1087d by Dmitry Kazakov.
Committed on 26/12/2016 at 18:53.
Pushed by dkazakov into branch 'kazakov/audio-track-support'.

Implement Audio Channel support

It is quite primitive yet (it doesn't have any visualisation), but it
works! Just select the file using a button on the timeline and it'll
work fine: with both playback and scrubbing.

TODO: icons for the button!

CC:kimageshop at kde.org

M  +5    -0    CMakeLists.txt
A  +4    -0    config-qtmultimedia.h.cmake
M  +33   -0    libs/image/kis_image_animation_interface.cpp
M  +26   -0    libs/image/kis_image_animation_interface.h
M  +6    -0    libs/ui/CMakeLists.txt
A  +107  -0    libs/ui/KisSyncedAudioPlayback.cpp     [License: UNKNOWN]  *
A  +29   -0    libs/ui/KisSyncedAudioPlayback.h     [License: UNKNOWN]  *
M  +77   -0    libs/ui/canvas/kis_animation_player.cpp
M  +6    -0    libs/ui/canvas/kis_animation_player.h
M  +10   -0    libs/ui/kis_config.cc
M  +3    -0    libs/ui/kis_config.h
M  +4    -1    plugins/dockers/animation/kis_time_based_item_model.cpp
M  +26   -0    plugins/dockers/animation/timeline_frames_model.cpp
M  +7    -0    plugins/dockers/animation/timeline_frames_model.h
M  +118  -0    plugins/dockers/animation/timeline_frames_view.cpp
M  +5    -0    plugins/dockers/animation/timeline_frames_view.h
M  +27   -0    plugins/impex/libkra/kis_kra_loader.cpp
M  +1    -0    plugins/impex/libkra/kis_kra_loader.h
M  +29   -0    plugins/impex/libkra/kis_kra_saver.cpp
M  +1    -0    plugins/impex/libkra/kis_kra_saver.h

The files marked with a * at the end have a non valid license. Please read: http://techbase.kde.org/Policies/Licensing_Policy and use the headers which are listed at that page.


https://commits.kde.org/krita/6419771ee0f77e0b9566381cf90d887124b1087d

diff --git a/CMakeLists.txt b/CMakeLists.txt
index 777319ca35e..43761012024 100644
--- a/CMakeLists.txt
+++ b/CMakeLists.txt
@@ -242,6 +242,8 @@ find_package(Qt5 ${MIN_QT_VERSION}
         Svg
         Test
         Concurrent
+        OPTIONAL_COMPONENTS
+        Multimedia
 )
 
 
@@ -292,6 +294,9 @@ else()
     set(HAVE_XCB FALSE)
 endif()
 
+macro_bool_to_01(Qt5Multimedia_FOUND HAVE_QT_MULTIMEDIA)
+configure_file(config-qtmultimedia.h.cmake ${CMAKE_CURRENT_BINARY_DIR}/config-qtmultimedia.h )
+
 add_definitions(
   -DQT_USE_QSTRINGBUILDER
   -DQT_STRICT_ITERATORS
diff --git a/config-qtmultimedia.h.cmake b/config-qtmultimedia.h.cmake
new file mode 100644
index 00000000000..ca655089f5f
--- /dev/null
+++ b/config-qtmultimedia.h.cmake
@@ -0,0 +1,4 @@
+/* config-qtmultimedia.h.  Generated by cmake from config-gsl.h.cmake */
+
+/* Defines if you have Qt Multimedia component */
+#cmakedefine HAVE_QT_MULTIMEDIA 1
diff --git a/libs/image/kis_image_animation_interface.cpp b/libs/image/kis_image_animation_interface.cpp
index 09cdc21f3ec..7a2ba32fae5 100644
--- a/libs/image/kis_image_animation_interface.cpp
+++ b/libs/image/kis_image_animation_interface.cpp
@@ -18,6 +18,8 @@
 
 #include "kis_image_animation_interface.h"
 
+#include <QFileInfo>
+
 #include "kis_global.h"
 #include "kis_image.h"
 #include "kis_regenerate_frame_stroke_strategy.h"
@@ -37,6 +39,7 @@ struct KisImageAnimationInterface::Private
           externalFrameActive(false),
           frameInvalidationBlocked(false),
           cachedLastFrameValue(-1),
+          audioChannelMuted(false),
           m_currentTime(0),
           m_currentUITime(0)
     {
@@ -50,6 +53,8 @@ struct KisImageAnimationInterface::Private
           playbackRange(rhs.playbackRange),
           framerate(rhs.framerate),
           cachedLastFrameValue(-1),
+          audioChannelFileName(rhs.audioChannelFileName),
+          audioChannelMuted(rhs.audioChannelMuted),
           m_currentTime(rhs.m_currentTime),
           m_currentUITime(rhs.m_currentUITime)
     {
@@ -63,6 +68,8 @@ struct KisImageAnimationInterface::Private
     KisTimeRange playbackRange;
     int framerate;
     int cachedLastFrameValue;
+    QString audioChannelFileName;
+    bool audioChannelMuted;
 
     KisSwitchTimeStrokeStrategy::SharedTokenWSP switchToken;
 
@@ -156,6 +163,32 @@ int KisImageAnimationInterface::framerate() const
     return m_d->framerate;
 }
 
+QString KisImageAnimationInterface::audioChannelFileName() const
+{
+    return m_d->audioChannelFileName;
+}
+
+void KisImageAnimationInterface::setAudioChannelFileName(const QString &fileName)
+{
+    QFileInfo info(fileName);
+
+    KIS_SAFE_ASSERT_RECOVER_NOOP(fileName.isEmpty() || info.isAbsolute());
+    m_d->audioChannelFileName = fileName.isEmpty() ? fileName : info.absoluteFilePath();
+
+    emit sigAudioChannelChanged();
+}
+
+bool KisImageAnimationInterface::isAudioMuted() const
+{
+    return m_d->audioChannelMuted;
+}
+
+void KisImageAnimationInterface::setAudioMuted(bool value)
+{
+    m_d->audioChannelMuted = value;
+    emit sigAudioChannelChanged();
+}
+
 void KisImageAnimationInterface::setFramerate(int fps)
 {
     m_d->framerate = fps;
diff --git a/libs/image/kis_image_animation_interface.h b/libs/image/kis_image_animation_interface.h
index b596c692924..c41b8ee0e43 100644
--- a/libs/image/kis_image_animation_interface.h
+++ b/libs/image/kis_image_animation_interface.h
@@ -122,6 +122,27 @@ public:
 
     int framerate() const;
 
+    /**
+     * @return **absolute** file name of the audio channel file
+     */
+    QString audioChannelFileName() const;
+
+    /**
+     * Sets **absolute** file name of the audio channel file. Dont' try to pass
+     * a relative path, it'll assert!
+     */
+    void setAudioChannelFileName(const QString &fileName);
+
+    /**
+     * @return is the audio channel is currently muted
+     */
+    bool isAudioMuted() const;
+
+    /**
+     * Mutes the audio channel
+     */
+    void setAudioMuted(bool value);
+
 public Q_SLOTS:
     void setFramerate(int fps);
 public:
@@ -158,6 +179,11 @@ Q_SIGNALS:
     void sigFullClipRangeChanged();
     void sigPlaybackRangeChanged();
 
+    /**
+     * Emitted when the audio channel of the document is changed
+     */
+    void sigAudioChannelChanged();
+
 private:
     struct Private;
     const QScopedPointer<Private> m_d;
diff --git a/libs/ui/CMakeLists.txt b/libs/ui/CMakeLists.txt
index 659ed48a7fe..07dce4a185f 100644
--- a/libs/ui/CMakeLists.txt
+++ b/libs/ui/CMakeLists.txt
@@ -374,6 +374,7 @@ if(WIN32)
         )
     include_directories(${Qt5Gui_PRIVATE_INCLUDE_DIRS})
 endif()
+
     set(kritaui_LIB_SRCS
         ${kritaui_LIB_SRCS}
         kis_animation_frame_cache.cpp
@@ -381,6 +382,7 @@ endif()
         canvas/kis_animation_player.cpp
         kis_animation_exporter.cpp
         kis_animation_importer.cpp
+        KisSyncedAudioPlayback.cpp
     )
 
 if(UNIX)
@@ -484,6 +486,10 @@ target_link_libraries(kritaui KF5::CoreAddons KF5::Completion KF5::I18n KF5::Ite
                       kritaimpex kritacolor kritaimage kritalibbrush kritawidgets kritawidgetutils ${PNG_LIBRARIES} ${EXIV2_LIBRARIES}
 )
 
+if (HAVE_QT_MULTIMEDIA)
+    target_link_libraries(kritaui Qt5::Multimedia)
+endif()
+
 if (HAVE_KIO)
     target_link_libraries(kritaui KF5::KIOCore)
 endif() 
diff --git a/libs/ui/KisSyncedAudioPlayback.cpp b/libs/ui/KisSyncedAudioPlayback.cpp
new file mode 100644
index 00000000000..afe9785d520
--- /dev/null
+++ b/libs/ui/KisSyncedAudioPlayback.cpp
@@ -0,0 +1,107 @@
+#include "KisSyncedAudioPlayback.h"
+
+#include "config-qtmultimedia.h"
+
+#ifdef HAVE_QT_MULTIMEDIA
+#include <QtMultimedia/QMediaPlayer>
+#else
+class QIODevice;
+
+#include <QUrl>
+
+namespace {
+
+struct QMediaPlayer {
+    enum State
+    {
+        StoppedState,
+        PlayingState,
+        PausedState
+    };
+
+    State state() const { return StoppedState; }
+
+    void play() {}
+    void stop() {}
+
+    qint64 position() const { return 0; }
+    qreal playbackRate() const { return 1.0; }
+    void setPosition(qint64) {}
+    void setPlaybackRate(qreal) {}
+    void setVolume(int) {}
+    void setMedia(const QUrl&, QIODevice * device = 0) { Q_UNUSED(device);}
+};
+}
+#endif
+
+
+
+#include <QFileInfo>
+
+
+
+struct KisSyncedAudioPlayback::Private
+{
+    QMediaPlayer player;
+    qint64 tolerance = 40;
+};
+
+
+KisSyncedAudioPlayback::KisSyncedAudioPlayback(const QString &fileName)
+    : QObject(0),
+      m_d(new Private)
+{
+    QFileInfo fileInfo(fileName);
+    Q_ASSERT(fileInfo.exists());
+
+    m_d->player.setMedia(QUrl::fromLocalFile(fileInfo.absoluteFilePath()));
+    m_d->player.setVolume(50);
+}
+
+KisSyncedAudioPlayback::~KisSyncedAudioPlayback()
+{
+}
+
+void KisSyncedAudioPlayback::setSoundOffsetTolerance(qint64 value)
+{
+    m_d->tolerance = value;
+}
+
+void KisSyncedAudioPlayback::syncWithVideo(qint64 position)
+{
+    if (qAbs(position - m_d->player.position()) > m_d->tolerance) {
+        m_d->player.setPosition(position);
+    }
+}
+
+bool KisSyncedAudioPlayback::isPlaying() const
+{
+    return m_d->player.state() == QMediaPlayer::PlayingState;
+}
+
+void KisSyncedAudioPlayback::setSpeed(qreal value)
+{
+    if (qFuzzyCompare(value, m_d->player.playbackRate())) return;
+
+    if (m_d->player.state() == QMediaPlayer::PlayingState) {
+        const qint64 oldPosition = m_d->player.position();
+
+        m_d->player.stop();
+        m_d->player.setPlaybackRate(value);
+        m_d->player.setPosition(oldPosition);
+        m_d->player.play();
+    } else {
+        m_d->player.setPlaybackRate(value);
+    }
+}
+
+void KisSyncedAudioPlayback::play(qint64 startPosition)
+{
+    m_d->player.setPosition(startPosition);
+    m_d->player.play();
+}
+
+void KisSyncedAudioPlayback::stop()
+{
+    m_d->player.stop();
+}
diff --git a/libs/ui/KisSyncedAudioPlayback.h b/libs/ui/KisSyncedAudioPlayback.h
new file mode 100644
index 00000000000..77b2496c01f
--- /dev/null
+++ b/libs/ui/KisSyncedAudioPlayback.h
@@ -0,0 +1,29 @@
+#ifndef KISSYNCEDAUDIOPLAYBACK_H
+#define KISSYNCEDAUDIOPLAYBACK_H
+
+#include <QScopedPointer>
+#include <QObject>
+
+class KisSyncedAudioPlayback : public QObject
+{
+    Q_OBJECT
+public:
+    KisSyncedAudioPlayback(const QString &fileName);
+    virtual ~KisSyncedAudioPlayback();
+
+    void setSoundOffsetTolerance(qint64 value);
+    void syncWithVideo(qint64 position);
+
+    bool isPlaying() const;
+
+public Q_SLOTS:
+    void setSpeed(qreal value);
+    void play(qint64 startPosition);
+    void stop();
+
+private:
+    struct Private;
+    const QScopedPointer<Private> m_d;
+};
+
+#endif // KISSYNCEDAUDIOPLAYBACK_H
diff --git a/libs/ui/canvas/kis_animation_player.cpp b/libs/ui/canvas/kis_animation_player.cpp
index 116a7f35699..6a32f99df76 100644
--- a/libs/ui/canvas/kis_animation_player.cpp
+++ b/libs/ui/canvas/kis_animation_player.cpp
@@ -35,11 +35,16 @@
 #include "kis_image_animation_interface.h"
 #include "kis_time_range.h"
 #include "kis_signal_compressor.h"
+#include <KisDocument.h>
+#include <QFileInfo>
 
 #include <boost/accumulators/accumulators.hpp>
 #include <boost/accumulators/statistics/stats.hpp>
 #include <boost/accumulators/statistics/rolling_mean.hpp>
 
+#include "KisSyncedAudioPlayback.h"
+#include "kis_signal_compressor_with_param.h"
+
 using namespace boost::accumulators;
 typedef accumulator_set<qreal, stats<tag::rolling_mean> > FpsAccumulator;
 
@@ -95,6 +100,9 @@ public:
 
     KisSignalCompressor playbackStatisticsCompressor;
 
+    QScopedPointer<KisSyncedAudioPlayback> syncedAudio;
+    QScopedPointer<KisSignalCompressorWithParam<int> > audioSyncScrubbingCompressor;
+
     void stopImpl(bool doUpdates);
 
     int incFrame(int frame, int inc) {
@@ -104,6 +112,13 @@ public:
         }
         return frame;
     }
+
+    qint64 frameToMSec(int value) {
+        return qreal(value) / fps * 1000.0;
+    }
+    int msecToFrame(qint64 value) {
+        return qreal(value) * fps / 1000.0;
+    }
 };
 
 KisAnimationPlayer::KisAnimationPlayer(KisCanvas2 *canvas)
@@ -127,6 +142,17 @@ KisAnimationPlayer::KisAnimationPlayer(KisCanvas2 *canvas)
 
     connect(&m_d->playbackStatisticsCompressor, SIGNAL(timeout()),
             this, SIGNAL(sigPlaybackStatisticsUpdated()));
+
+    using namespace std::placeholders;
+    std::function<void (int)> callback(
+        std::bind(&KisAnimationPlayer::slotSyncScrubbingAudio, this, _1));
+
+    KisConfig cfg;
+    m_d->audioSyncScrubbingCompressor.reset(
+        new KisSignalCompressorWithParam<int>(cfg.scribbingAudioUpdatesDelay(), callback, KisSignalCompressor::FIRST_ACTIVE));
+
+    connect(m_d->canvas->image()->animationInterface(), SIGNAL(sigAudioChannelChanged()), SLOT(slotAudioChannelChanged()));
+    slotAudioChannelChanged();
 }
 
 KisAnimationPlayer::~KisAnimationPlayer()
@@ -138,6 +164,24 @@ void KisAnimationPlayer::slotUpdateDropFramesMode()
     m_d->dropFramesMode = cfg.animationDropFrames();
 }
 
+void KisAnimationPlayer::slotSyncScrubbingAudio(int msecTime)
+{
+    KIS_SAFE_ASSERT_RECOVER_RETURN(m_d->syncedAudio);
+    m_d->syncedAudio->syncWithVideo(msecTime);
+}
+
+void KisAnimationPlayer::slotAudioChannelChanged()
+{
+
+    QString fileName = m_d->canvas->image()->animationInterface()->audioChannelFileName();
+    QFileInfo info(fileName);
+    if (info.exists() && !m_d->canvas->image()->animationInterface()->isAudioMuted()) {
+        m_d->syncedAudio.reset(new KisSyncedAudioPlayback(info.absoluteFilePath()));
+    } else {
+        m_d->syncedAudio.reset();
+    }
+}
+
 void KisAnimationPlayer::connectCancelSignals()
 {
     m_d->cancelStrokeConnections.addConnection(
@@ -187,6 +231,11 @@ void KisAnimationPlayer::slotUpdatePlaybackTimer()
 
     m_d->expectedInterval = qreal(1000) / m_d->fps / m_d->playbackSpeed;
     m_d->lastTimerInterval = m_d->expectedInterval;
+
+    if (m_d->syncedAudio) {
+        m_d->syncedAudio->setSpeed(m_d->playbackSpeed);
+    }
+
     m_d->timer->start(m_d->expectedInterval);
 
     if (m_d->playbackTime.isValid()) {
@@ -207,10 +256,18 @@ void KisAnimationPlayer::play()
     m_d->lastPaintedFrame = m_d->firstFrame;
 
     connectCancelSignals();
+
+    if (m_d->syncedAudio) {
+        m_d->syncedAudio->play(m_d->frameToMSec(m_d->firstFrame));
+    }
 }
 
 void KisAnimationPlayer::Private::stopImpl(bool doUpdates)
 {
+    if (syncedAudio) {
+        syncedAudio->stop();
+    }
+
     q->disconnectCancelSignals();
 
     timer->stop();
@@ -258,6 +315,17 @@ void KisAnimationPlayer::slotUpdate()
     uploadFrame(-1);
 }
 
+void KisAnimationPlayer::setScrubState(bool value, int currentFrame)
+{
+    if (!m_d->syncedAudio || isPlaying()) return;
+
+    if (value) {
+        m_d->syncedAudio->play(m_d->frameToMSec(currentFrame));
+    } else {
+        m_d->syncedAudio->stop();
+    }
+}
+
 void KisAnimationPlayer::uploadFrame(int frame)
 {
     if (frame < 0) {
@@ -287,6 +355,15 @@ void KisAnimationPlayer::uploadFrame(int frame)
         m_d->playbackStatisticsCompressor.start();
     }
 
+    if (m_d->syncedAudio) {
+        const int msecTime = m_d->frameToMSec(frame);
+        if (isPlaying()) {
+            slotSyncScrubbingAudio(msecTime);
+        } else {
+            m_d->audioSyncScrubbingCompressor->start(msecTime);
+        }
+    }
+
     if (m_d->canvas->frameCache() && m_d->canvas->frameCache()->uploadFrame(frame)) {
         m_d->canvas->updateCanvas();
 
diff --git a/libs/ui/canvas/kis_animation_player.h b/libs/ui/canvas/kis_animation_player.h
index 06e1d357168..6456e97855d 100644
--- a/libs/ui/canvas/kis_animation_player.h
+++ b/libs/ui/canvas/kis_animation_player.h
@@ -50,6 +50,8 @@ public:
     qreal realFps() const;
     qreal framesDroppedPortion() const;
 
+    void setScrubState(bool value, int currentFrame);
+
 public Q_SLOTS:
     void slotUpdate();
     void slotCancelPlayback();
@@ -58,6 +60,10 @@ public Q_SLOTS:
     void slotUpdatePlaybackTimer();
     void slotUpdateDropFramesMode();
 
+private Q_SLOTS:
+    void slotSyncScrubbingAudio(int msecTime);
+    void slotAudioChannelChanged();
+
 Q_SIGNALS:
     void sigFrameChanged();
     void sigPlaybackStopped();
diff --git a/libs/ui/kis_config.cc b/libs/ui/kis_config.cc
index 60ed252e378..43eb97b2baf 100644
--- a/libs/ui/kis_config.cc
+++ b/libs/ui/kis_config.cc
@@ -1655,6 +1655,16 @@ void KisConfig::setScribbingUpdatesDelay(int value)
     m_cfg.writeEntry("scribbingUpdatesDelay", value);
 }
 
+int KisConfig::scribbingAudioUpdatesDelay(bool defaultValue) const
+{
+    return (defaultValue ? 200 : m_cfg.readEntry("scribbingAudioUpdatesDelay", 200));
+}
+
+void KisConfig::setScribbingAudioUpdatesDelay(int value)
+{
+    m_cfg.writeEntry("scribbingAudioUpdatesDelay", value);
+}
+
 bool KisConfig::switchSelectionCtrlAlt(bool defaultValue) const
 {
     return defaultValue ? false : m_cfg.readEntry("switchSelectionCtrlAlt", false);
diff --git a/libs/ui/kis_config.h b/libs/ui/kis_config.h
index 4f2fede1018..640e94cfae9 100644
--- a/libs/ui/kis_config.h
+++ b/libs/ui/kis_config.h
@@ -473,6 +473,9 @@ public:
     int scribbingUpdatesDelay(bool defaultValue = false) const;
     void setScribbingUpdatesDelay(int value);
 
+    int scribbingAudioUpdatesDelay(bool defaultValue = false) const;
+    void setScribbingAudioUpdatesDelay(int value);
+
     bool switchSelectionCtrlAlt(bool defaultValue = false) const;
     void setSwitchSelectionCtrlAlt(bool value);
 
diff --git a/plugins/dockers/animation/kis_time_based_item_model.cpp b/plugins/dockers/animation/kis_time_based_item_model.cpp
index 99ff688ca15..620cb9512f5 100644
--- a/plugins/dockers/animation/kis_time_based_item_model.cpp
+++ b/plugins/dockers/animation/kis_time_based_item_model.cpp
@@ -54,7 +54,6 @@ struct KisTimeBasedItemModel::Private
 
     QScopedPointer<KisSignalCompressorWithParam<int> > scrubbingCompressor;
 
-
     int baseNumFrames() const {
         if (image.isNull()) return 0;
 
@@ -345,6 +344,10 @@ void KisTimeBasedItemModel::setScrubState(bool active)
         m_d->scrubInProgress = true;
     }
 
+    if (m_d->animationPlayer && !m_d->animationPlayer->isPlaying()) {
+        m_d->animationPlayer->setScrubState(active, m_d->activeFrameIndex);
+    }
+
     if (m_d->scrubInProgress && !active) {
 
         m_d->scrubInProgress = false;
diff --git a/plugins/dockers/animation/timeline_frames_model.cpp b/plugins/dockers/animation/timeline_frames_model.cpp
index daed88e26f7..51c5e02e2df 100644
--- a/plugins/dockers/animation/timeline_frames_model.cpp
+++ b/plugins/dockers/animation/timeline_frames_model.cpp
@@ -220,6 +220,7 @@ void TimelineFramesModel::setDummiesFacade(KisDummiesFacadeBase *dummiesFacade,
     KisDummiesFacadeBase *oldDummiesFacade = m_d->dummiesFacade;
 
     if (m_d->dummiesFacade) {
+        m_d->image->animationInterface()->disconnect(this);
         m_d->image->disconnect(this);
         m_d->dummiesFacade->disconnect(this);
     }
@@ -236,6 +237,8 @@ void TimelineFramesModel::setDummiesFacade(KisDummiesFacadeBase *dummiesFacade,
                 SLOT(slotDummyChanged(KisNodeDummy*)));
         connect(m_d->image->animationInterface(),
                 SIGNAL(sigFullClipRangeChanged()), SIGNAL(sigInfiniteTimelineUpdateNeeded()));
+        connect(m_d->image->animationInterface(),
+                SIGNAL(sigAudioChannelChanged()), SIGNAL(sigAudioChannelChanged()));
     }
 
     if (m_d->dummiesFacade != oldDummiesFacade) {
@@ -244,6 +247,7 @@ void TimelineFramesModel::setDummiesFacade(KisDummiesFacadeBase *dummiesFacade,
 
     if (m_d->dummiesFacade) {
         emit sigInfiniteTimelineUpdateNeeded();
+        emit sigAudioChannelChanged();
     }
 }
 
@@ -643,3 +647,25 @@ bool TimelineFramesModel::copyFrame(const QModelIndex &dstIndex)
 
     return result;
 }
+
+QString TimelineFramesModel::audioChannelFileName() const
+{
+    return m_d->image ? m_d->image->animationInterface()->audioChannelFileName() : QString();
+}
+
+void TimelineFramesModel::setAudioChannelFileName(const QString &fileName)
+{
+    KIS_SAFE_ASSERT_RECOVER_RETURN(m_d->image);
+    m_d->image->animationInterface()->setAudioChannelFileName(fileName);
+}
+
+bool TimelineFramesModel::isAudioMuted() const
+{
+    return m_d->image ? m_d->image->animationInterface()->isAudioMuted() : false;
+}
+
+void TimelineFramesModel::setAudioMuted(bool value)
+{
+    KIS_SAFE_ASSERT_RECOVER_RETURN(m_d->image);
+    m_d->image->animationInterface()->setAudioMuted(value);
+}
diff --git a/plugins/dockers/animation/timeline_frames_model.h b/plugins/dockers/animation/timeline_frames_model.h
index 3c147ad8a87..475a215cfd6 100644
--- a/plugins/dockers/animation/timeline_frames_model.h
+++ b/plugins/dockers/animation/timeline_frames_model.h
@@ -51,6 +51,12 @@ public:
     bool createFrame(const QModelIndex &dstIndex);
     bool copyFrame(const QModelIndex &dstIndex);
 
+    QString audioChannelFileName() const;
+    void setAudioChannelFileName(const QString &fileName);
+
+    bool isAudioMuted() const;
+    void setAudioMuted(bool value);
+
     void setLastClickedIndex(const QModelIndex &index);
 
     int rowCount(const QModelIndex &parent = QModelIndex()) const;
@@ -112,6 +118,7 @@ public Q_SLOTS:
 Q_SIGNALS:
     void requestCurrentNodeChanged(KisNodeSP node);
     void sigInfiniteTimelineUpdateNeeded();
+    void sigAudioChannelChanged();
 
 private:
     struct Private;
diff --git a/plugins/dockers/animation/timeline_frames_view.cpp b/plugins/dockers/animation/timeline_frames_view.cpp
index e0c836ad6e3..a76848e2563 100644
--- a/plugins/dockers/animation/timeline_frames_view.cpp
+++ b/plugins/dockers/animation/timeline_frames_view.cpp
@@ -28,6 +28,7 @@
 
 #include <QPainter>
 
+#include <QFileInfo>
 #include <QApplication>
 #include <QHeaderView>
 #include <QDropEvent>
@@ -54,6 +55,11 @@
 #include "kis_time_range.h"
 #include "kis_color_label_selector_widget.h"
 
+#include <KoFileDialog.h>
+#include <QDesktopServices>
+
+#include "config-qtmultimedia.h"
+
 typedef QPair<QRect, QModelIndex> QItemViewPaintPair;
 typedef QList<QItemViewPaintPair> QItemViewPaintPairs;
 
@@ -84,11 +90,17 @@ struct TimelineFramesView::Private
     QToolButton *addLayersButton;
     KisAction *showHideLayerAction;
 
+    QToolButton *audioOptionsButton;
+
     KisColorLabelSelectorWidget *colorSelector;
     QWidgetAction *colorSelectorAction;
     KisColorLabelSelectorWidget *multiframeColorSelector;
     QWidgetAction *multiframeColorSelectorAction;
 
+    QMenu *audioOptionsMenu;
+    QAction *openAudioAction;
+    QAction *audioMuteAction;
+
     QMenu *layerEditingMenu;
     QMenu *existingLayersMenu;
 
@@ -149,6 +161,9 @@ TimelineFramesView::TimelineFramesView(QWidget *parent)
     connect(horizontalScrollBar(), SIGNAL(valueChanged(int)), SLOT(slotUpdateInfiniteFramesCount()));
     connect(horizontalScrollBar(), SIGNAL(sliderReleased()), SLOT(slotUpdateInfiniteFramesCount()));
 
+
+    /********** New Layer Menu ***********************************************************/
+
     m_d->addLayersButton = new QToolButton(this);
     m_d->addLayersButton->setAutoRaise(true);
     m_d->addLayersButton->setIcon(KisIconUtils::loadIcon("addlayer"));
@@ -176,6 +191,36 @@ TimelineFramesView::TimelineFramesView(QWidget *parent)
 
     m_d->addLayersButton->setMenu(m_d->layerEditingMenu);
 
+    /********** Audio Channel Menu *******************************************************/
+
+    m_d->audioOptionsButton = new QToolButton(this);
+    m_d->audioOptionsButton->setAutoRaise(true);
+    m_d->audioOptionsButton->setIcon(KisIconUtils::loadIcon("zoom-horizontal"));
+    m_d->audioOptionsButton->setSizePolicy(QSizePolicy::Minimum, QSizePolicy::Minimum);
+    m_d->audioOptionsButton->setPopupMode(QToolButton::InstantPopup);
+
+    m_d->audioOptionsMenu = new QMenu(this);
+
+#ifndef HAVE_QT_MULTIMEDIA
+    m_d->audioOptionsMenu->addSection(i18nc("@item:inmenu", "Audio playback is not suported in this build!"));
+#endif
+
+    m_d->openAudioAction= new QAction("XXX", this);
+    connect(m_d->openAudioAction, SIGNAL(triggered()), this, SLOT(slotSelectAudioChannelFile()));
+    m_d->audioOptionsMenu->addAction(m_d->openAudioAction);
+
+    m_d->audioMuteAction = new QAction(i18nc("@item:inmenu", "Mute"), this);
+    m_d->audioMuteAction->setCheckable(true);
+    connect(m_d->audioMuteAction, SIGNAL(triggered(bool)), SLOT(slotAudioChannelMute(bool)));
+
+    m_d->audioOptionsMenu->addAction(m_d->audioMuteAction);
+
+    m_d->audioOptionsMenu->addAction(i18nc("@item:inmenu", "Remove audio"), this, SLOT(slotAudioChannelRemove()));
+
+    m_d->audioOptionsButton->setMenu(m_d->audioOptionsMenu);
+
+    /********** Frame Editing Context Menu ***********************************************/
+
     m_d->frameCreationMenu = new QMenu(this);
     m_d->frameCreationMenu->addAction(KisAnimationUtils::addFrameActionName, this, SLOT(slotNewFrame()));
     m_d->frameCreationMenu->addAction(KisAnimationUtils::duplicateFrameActionName, this, SLOT(slotCopyFrame()));
@@ -198,6 +243,8 @@ TimelineFramesView::TimelineFramesView(QWidget *parent)
     m_d->multipleFrameEditingMenu->addAction(KisAnimationUtils::removeFramesActionName, this, SLOT(slotRemoveFrame()));
     m_d->multipleFrameEditingMenu->addAction(m_d->multiframeColorSelectorAction);
 
+    /********** Zoom Button **************************************************************/
+
     m_d->zoomDragButton = new KisZoomButton(this);
     m_d->zoomDragButton->setAutoRaise(true);
     m_d->zoomDragButton->setIcon(KisIconUtils::loadIcon("zoom-horizontal"));
@@ -240,11 +287,13 @@ void TimelineFramesView::updateGeometries()
     const int minimalSize = availableHeight - 2 * margin;
 
     resizeToMinimalSize(m_d->addLayersButton, minimalSize);
+    resizeToMinimalSize(m_d->audioOptionsButton, minimalSize);
     resizeToMinimalSize(m_d->zoomDragButton, minimalSize);
 
     int x = 2 * margin;
     int y = (availableHeight - minimalSize) / 2;
     m_d->addLayersButton->move(x, 2 * y);
+    m_d->audioOptionsButton->move(x + minimalSize + 2 * margin, 2 * y);
 
     const int availableWidth = m_d->layersHeader->width();
 
@@ -271,8 +320,13 @@ void TimelineFramesView::setModel(QAbstractItemModel *model)
     connect(m_d->model, SIGNAL(sigInfiniteTimelineUpdateNeeded()),
             this, SLOT(slotUpdateInfiniteFramesCount()));
 
+    connect(m_d->model, SIGNAL(sigAudioChannelChanged()),
+            this, SLOT(slotUpdateAudioActions()));
+
     connect(selectionModel(), SIGNAL(selectionChanged(const QItemSelection &, const QItemSelection &)),
             &m_d->selectionChangedCompressor, SLOT(start()));
+
+    slotUpdateAudioActions();
 }
 
 void TimelineFramesView::setFramesPerSecond(int fps)
@@ -320,6 +374,70 @@ void TimelineFramesView::slotColorLabelChanged(int label)
     config.setDefaultFrameColorLabel(label);
 }
 
+void TimelineFramesView::slotSelectAudioChannelFile()
+{
+    if (!m_d->model) return;
+
+    KoFileDialog dialog(this, KoFileDialog::ImportFiles, "ImportAudio");
+
+    QString defaultDir = QDesktopServices::storageLocation(QDesktopServices::MusicLocation);
+
+    const QString currentFile = m_d->model->audioChannelFileName();
+    QDir baseDir = QFileInfo(currentFile).absoluteDir();
+    if (baseDir.exists()) {
+        defaultDir = baseDir.absolutePath();
+    }
+
+    dialog.setDefaultDir(defaultDir);
+
+    QStringList mimeTypes;
+    mimeTypes << "audio/mpeg";
+    mimeTypes << "audio/ogg";
+    mimeTypes << "audio/vorbis";
+    mimeTypes << "audio/vnd.wave";
+
+    dialog.setMimeTypeFilters(mimeTypes);
+    dialog.setCaption(i18nc("@titile:window", "Open Audio"));
+
+    const QString result = dialog.filename();
+    const QFileInfo info(result);
+
+    if (info.exists()) {
+        m_d->model->setAudioChannelFileName(info.absoluteFilePath());
+    }
+}
+
+void TimelineFramesView::slotAudioChannelMute(bool value)
+{
+    if (!m_d->model) return;
+
+    if (value != m_d->model->isAudioMuted()) {
+        m_d->model->setAudioMuted(value);
+    }
+}
+
+void TimelineFramesView::slotAudioChannelRemove()
+{
+    if (!m_d->model) return;
+    m_d->model->setAudioChannelFileName(QString());
+}
+
+void TimelineFramesView::slotUpdateAudioActions()
+{
+    if (!m_d->model) return;
+
+    const QString currentFile = m_d->model->audioChannelFileName();
+
+    if (currentFile.isEmpty()) {
+        m_d->openAudioAction->setText(i18nc("@item:inmenu", "Open audio..."));
+    } else {
+        QFileInfo info(currentFile);
+        m_d->openAudioAction->setText(i18nc("@item:inmenu", "Change audio (%1)...", info.fileName()));
+    }
+
+    m_d->audioMuteAction->setChecked(m_d->model->isAudioMuted());
+}
+
 void TimelineFramesView::slotUpdateInfiniteFramesCount()
 {
     if (horizontalScrollBar()->isSliderDown()) return;
diff --git a/plugins/dockers/animation/timeline_frames_view.h b/plugins/dockers/animation/timeline_frames_view.h
index 1f395216146..cef58a27550 100644
--- a/plugins/dockers/animation/timeline_frames_view.h
+++ b/plugins/dockers/animation/timeline_frames_view.h
@@ -71,6 +71,11 @@ private Q_SLOTS:
 
     void slotColorLabelChanged(int);
 
+    void slotSelectAudioChannelFile();
+    void slotAudioChannelMute(bool value);
+    void slotAudioChannelRemove();
+    void slotUpdateAudioActions();
+
 private:
     void setFramesPerSecond(int fps);
 
diff --git a/plugins/impex/libkra/kis_kra_loader.cpp b/plugins/impex/libkra/kis_kra_loader.cpp
index b076d719bbc..937ed5b9455 100644
--- a/plugins/impex/libkra/kis_kra_loader.cpp
+++ b/plugins/impex/libkra/kis_kra_loader.cpp
@@ -342,6 +342,8 @@ KisImageSP KisKraLoader::loadXML(const KoXmlElement& element)
             loadGuides(e);
         } else if (e.tagName() == "assistants") {
             loadAssistantsList(e);
+        } else if (e.tagName() == "audio") {
+            loadAudio(e, image);
         }
     }
 
@@ -1103,3 +1105,28 @@ void KisKraLoader::loadGuides(const KoXmlElement& elem)
     guides.loadFromXml(domElement);
     m_d->document->setGuidesConfig(guides);
 }
+
+void KisKraLoader::loadAudio(const KoXmlElement& elem, KisImageSP image)
+{
+    QDomDocument dom;
+    KoXml::asQDomElement(dom, elem);
+    QDomElement qElement = dom.firstChildElement();
+
+    QString fileName;
+    if (KisDomUtils::loadValue(qElement, "masterChannelPath", &fileName)) {
+        fileName = QDir::toNativeSeparators(fileName);
+
+        QDir baseDirectory = QFileInfo(m_d->document->localFilePath()).absoluteDir();
+        qDebug() << ppVar(baseDirectory);
+
+        fileName = baseDirectory.absoluteFilePath(fileName);
+
+        QFileInfo info(fileName);
+
+        if (!info.exists()) {
+            m_d->errorMessages << i18n("Audio channel file %1 doesn't exist!", fileName);
+        } else {
+            image->animationInterface()->setAudioChannelFileName(info.absoluteFilePath());
+        }
+    }
+}
diff --git a/plugins/impex/libkra/kis_kra_loader.h b/plugins/impex/libkra/kis_kra_loader.h
index 168dc243ad4..789cfdad2d8 100644
--- a/plugins/impex/libkra/kis_kra_loader.h
+++ b/plugins/impex/libkra/kis_kra_loader.h
@@ -103,6 +103,7 @@ private:
     void loadAssistantsList(const KoXmlElement& elem);
     void loadGrid(const KoXmlElement& elem);
     void loadGuides(const KoXmlElement& elem);
+    void loadAudio(const KoXmlElement& elem, KisImageSP image);
 private:
 
     struct Private;
diff --git a/plugins/impex/libkra/kis_kra_saver.cpp b/plugins/impex/libkra/kis_kra_saver.cpp
index e74571800bb..bd9abaaa95e 100644
--- a/plugins/impex/libkra/kis_kra_saver.cpp
+++ b/plugins/impex/libkra/kis_kra_saver.cpp
@@ -57,6 +57,9 @@
 #include "kis_guides_config.h"
 #include "KisProofingConfiguration.h"
 
+#include <QFileInfo>
+#include <QDir>
+
 
 using namespace KRA;
 
@@ -126,6 +129,7 @@ QDomElement KisKraSaver::saveXML(QDomDocument& doc,  KisImageSP image)
     saveAssistantsList(doc, imageElement);
     saveGrid(doc,imageElement);
     saveGuides(doc,imageElement);
+    saveAudio(doc,imageElement);
 
     QDomElement animationElement = doc.createElement("animation");
     KisDomUtils::saveValue(&animationElement, "framerate", image->animationInterface()->framerate());
@@ -424,3 +428,28 @@ bool KisKraSaver::saveGuides(QDomDocument& doc, QDomElement& element)
     return true;
 }
 
+bool KisKraSaver::saveAudio(QDomDocument& doc, QDomElement& element)
+{
+    QString fileName = m_d->doc->image()->animationInterface()->audioChannelFileName();
+    if (fileName.isEmpty()) return true;
+
+    if (!QFileInfo::exists(fileName)) {
+        m_d->errorMessages << i18n("Audio channel file %1 doesn't exist!", fileName);
+        return false;
+    }
+
+    const QDir documentDir = QFileInfo(m_d->doc->localFilePath()).absoluteDir();
+    KIS_ASSERT_RECOVER_RETURN_VALUE(documentDir.exists(), false);
+
+    fileName = documentDir.relativeFilePath(fileName);
+    fileName = QDir::fromNativeSeparators(fileName);
+
+    KIS_ASSERT_RECOVER_RETURN_VALUE(!fileName.isEmpty(), false);
+
+    QDomElement audioElement = doc.createElement("audio");
+    KisDomUtils::saveValue(&audioElement, "masterChannelPath", fileName);
+    element.appendChild(audioElement);
+
+    return true;
+}
+
diff --git a/plugins/impex/libkra/kis_kra_saver.h b/plugins/impex/libkra/kis_kra_saver.h
index 810abcfd7ca..ae8368b9575 100644
--- a/plugins/impex/libkra/kis_kra_saver.h
+++ b/plugins/impex/libkra/kis_kra_saver.h
@@ -55,6 +55,7 @@ private:
     bool saveAssistantsList(QDomDocument& doc, QDomElement& element);
     bool saveGrid(QDomDocument& doc, QDomElement& element);
     bool saveGuides(QDomDocument& doc, QDomElement& element);
+    bool saveAudio(QDomDocument& doc, QDomElement& element);
     bool saveNodeKeyframes(KoStore *store, QString location, const KisNode *node);
     struct Private;
     Private * const m_d;


More information about the kimageshop mailing list