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@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 whic= h 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}/con= fig-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 + #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 &fi= leName) +{ + QFileInfo info(fileName); + + KIS_SAFE_ASSERT_RECOVER_NOOP(fileName.isEmpty() || info.isAbsolute()); + m_d->audioChannelFileName =3D fileName.isEmpty() ? fileName : info.abs= oluteFilePath(); + + emit sigAudioChannelChanged(); +} + +bool KisImageAnimationInterface::isAudioMuted() const +{ + return m_d->audioChannelMuted; +} + +void KisImageAnimationInterface::setAudioMuted(bool value) +{ + m_d->audioChannelMuted =3D value; + emit sigAudioChannelChanged(); +} + void KisImageAnimationInterface::setFramerate(int fps) { m_d->framerate =3D fps; diff --git a/libs/image/kis_image_animation_interface.h b/libs/image/kis_im= age_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 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::Com= pletion KF5::I18n KF5::Ite kritaimpex kritacolor kritaimage kritalibbrush krita= widgets 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/KisSyncedAudioPla= yback.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 +#else +class QIODevice; + +#include + +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 =3D 0) { Q_UNUSED(device= );} +}; +} +#endif + + + +#include + + + +struct KisSyncedAudioPlayback::Private +{ + QMediaPlayer player; + qint64 tolerance =3D 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 =3D 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() =3D=3D QMediaPlayer::PlayingState; +} + +void KisSyncedAudioPlayback::setSpeed(qreal value) +{ + if (qFuzzyCompare(value, m_d->player.playbackRate())) return; + + if (m_d->player.state() =3D=3D QMediaPlayer::PlayingState) { + const qint64 oldPosition =3D 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/KisSyncedAudioPlayb= ack.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 +#include + +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 m_d; +}; + +#endif // KISSYNCEDAUDIOPLAYBACK_H diff --git a/libs/ui/canvas/kis_animation_player.cpp b/libs/ui/canvas/kis_a= nimation_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 +#include = #include #include #include = +#include "KisSyncedAudioPlayback.h" +#include "kis_signal_compressor_with_param.h" + using namespace boost::accumulators; typedef accumulator_set > FpsAccumulator; = @@ -95,6 +100,9 @@ public: = KisSignalCompressor playbackStatisticsCompressor; = + QScopedPointer syncedAudio; + QScopedPointer > audioSyncScrubbingC= ompressor; + 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 *can= vas) = connect(&m_d->playbackStatisticsCompressor, SIGNAL(timeout()), this, SIGNAL(sigPlaybackStatisticsUpdated())); + + using namespace std::placeholders; + std::function callback( + std::bind(&KisAnimationPlayer::slotSyncScrubbingAudio, this, _1)); + + KisConfig cfg; + m_d->audioSyncScrubbingCompressor.reset( + new KisSignalCompressorWithParam(cfg.scribbingAudioUpdatesDel= ay(), callback, KisSignalCompressor::FIRST_ACTIVE)); + + connect(m_d->canvas->image()->animationInterface(), SIGNAL(sigAudioCha= nnelChanged()), SLOT(slotAudioChannelChanged())); + slotAudioChannelChanged(); } = KisAnimationPlayer::~KisAnimationPlayer() @@ -138,6 +164,24 @@ void KisAnimationPlayer::slotUpdateDropFramesMode() m_d->dropFramesMode =3D 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 =3D m_d->canvas->image()->animationInterface()->audio= ChannelFileName(); + QFileInfo info(fileName); + if (info.exists() && !m_d->canvas->image()->animationInterface()->isAu= dioMuted()) { + m_d->syncedAudio.reset(new KisSyncedAudioPlayback(info.absoluteFil= ePath())); + } else { + m_d->syncedAudio.reset(); + } +} + void KisAnimationPlayer::connectCancelSignals() { m_d->cancelStrokeConnections.addConnection( @@ -187,6 +231,11 @@ void KisAnimationPlayer::slotUpdatePlaybackTimer() = m_d->expectedInterval =3D qreal(1000) / m_d->fps / m_d->playbackSpeed; m_d->lastTimerInterval =3D 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 =3D 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 =3D m_d->frameToMSec(frame); + if (isPlaying()) { + slotSyncScrubbingAudio(msecTime); + } else { + m_d->audioSyncScrubbingCompressor->start(msecTime); + } + } + if (m_d->canvas->frameCache() && m_d->canvas->frameCache()->uploadFram= e(frame)) { m_d->canvas->updateCanvas(); = diff --git a/libs/ui/canvas/kis_animation_player.h b/libs/ui/canvas/kis_ani= mation_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("scribbingAudioUpdatesDel= ay", 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 =3D false) const; void setScribbingUpdatesDelay(int value); = + int scribbingAudioUpdatesDelay(bool defaultValue =3D false) const; + void setScribbingAudioUpdatesDelay(int value); + bool switchSelectionCtrlAlt(bool defaultValue =3D false) const; void setSwitchSelectionCtrlAlt(bool value); = diff --git a/plugins/dockers/animation/kis_time_based_item_model.cpp b/plug= ins/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 > scrubbingCompressor; = - int baseNumFrames() const { if (image.isNull()) return 0; = @@ -345,6 +344,10 @@ void KisTimeBasedItemModel::setScrubState(bool active) m_d->scrubInProgress =3D 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 =3D 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(KisDummiesFa= cadeBase *dummiesFacade, KisDummiesFacadeBase *oldDummiesFacade =3D 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(KisDummiesFa= cadeBase *dummiesFacade, SLOT(slotDummyChanged(KisNodeDummy*))); connect(m_d->image->animationInterface(), SIGNAL(sigFullClipRangeChanged()), SIGNAL(sigInfiniteTimel= ineUpdateNeeded())); + connect(m_d->image->animationInterface(), + SIGNAL(sigAudioChannelChanged()), SIGNAL(sigAudioChannelCh= anged())); } = if (m_d->dummiesFacade !=3D oldDummiesFacade) { @@ -244,6 +247,7 @@ void TimelineFramesModel::setDummiesFacade(KisDummiesFa= cadeBase *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()->audioChannelFile= Name() : 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/do= ckers/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 =3D 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/d= ockers/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 = +#include #include #include #include @@ -54,6 +55,11 @@ #include "kis_time_range.h" #include "kis_color_label_selector_widget.h" = +#include +#include + +#include "config-qtmultimedia.h" + typedef QPair QItemViewPaintPair; typedef QList 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(slotUpd= ateInfiniteFramesCount())); connect(horizontalScrollBar(), SIGNAL(sliderReleased()), SLOT(slotUpda= teInfiniteFramesCount())); = + + /********** New Layer Menu *******************************************= ****************/ + m_d->addLayersButton =3D 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 =3D new QToolButton(this); + m_d->audioOptionsButton->setAutoRaise(true); + m_d->audioOptionsButton->setIcon(KisIconUtils::loadIcon("zoom-horizont= al")); + m_d->audioOptionsButton->setSizePolicy(QSizePolicy::Minimum, QSizePoli= cy::Minimum); + m_d->audioOptionsButton->setPopupMode(QToolButton::InstantPopup); + + m_d->audioOptionsMenu =3D new QMenu(this); + +#ifndef HAVE_QT_MULTIMEDIA + m_d->audioOptionsMenu->addSection(i18nc("@item:inmenu", "Audio playbac= k is not suported in this build!")); +#endif + + m_d->openAudioAction=3D new QAction("XXX", this); + connect(m_d->openAudioAction, SIGNAL(triggered()), this, SLOT(slotSele= ctAudioChannelFile())); + m_d->audioOptionsMenu->addAction(m_d->openAudioAction); + + m_d->audioMuteAction =3D new QAction(i18nc("@item:inmenu", "Mute"), th= is); + m_d->audioMuteAction->setCheckable(true); + connect(m_d->audioMuteAction, SIGNAL(triggered(bool)), SLOT(slotAudioC= hannelMute(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 =3D new QMenu(this); m_d->frameCreationMenu->addAction(KisAnimationUtils::addFrameActionNam= e, this, SLOT(slotNewFrame())); m_d->frameCreationMenu->addAction(KisAnimationUtils::duplicateFrameAct= ionName, this, SLOT(slotCopyFrame())); @@ -198,6 +243,8 @@ TimelineFramesView::TimelineFramesView(QWidget *parent) m_d->multipleFrameEditingMenu->addAction(KisAnimationUtils::removeFram= esActionName, this, SLOT(slotRemoveFrame())); m_d->multipleFrameEditingMenu->addAction(m_d->multiframeColorSelectorA= ction); = + /********** Zoom Button **********************************************= ****************/ + m_d->zoomDragButton =3D 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 =3D availableHeight - 2 * margin; = resizeToMinimalSize(m_d->addLayersButton, minimalSize); + resizeToMinimalSize(m_d->audioOptionsButton, minimalSize); resizeToMinimalSize(m_d->zoomDragButton, minimalSize); = int x =3D 2 * margin; int y =3D (availableHeight - minimalSize) / 2; m_d->addLayersButton->move(x, 2 * y); + m_d->audioOptionsButton->move(x + minimalSize + 2 * margin, 2 * y); = const int availableWidth =3D 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 lab= el) config.setDefaultFrameColorLabel(label); } = +void TimelineFramesView::slotSelectAudioChannelFile() +{ + if (!m_d->model) return; + + KoFileDialog dialog(this, KoFileDialog::ImportFiles, "ImportAudio"); + + QString defaultDir =3D QDesktopServices::storageLocation(QDesktopServi= ces::MusicLocation); + + const QString currentFile =3D m_d->model->audioChannelFileName(); + QDir baseDir =3D QFileInfo(currentFile).absoluteDir(); + if (baseDir.exists()) { + defaultDir =3D 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 =3D 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 !=3D 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 =3D 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/doc= kers/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& el= ement) loadGuides(e); } else if (e.tagName() =3D=3D "assistants") { loadAssistantsList(e); + } else if (e.tagName() =3D=3D "audio") { + loadAudio(e, image); } } = @@ -1103,3 +1105,28 @@ void KisKraLoader::loadGuides(const KoXmlElement& el= em) guides.loadFromXml(domElement); m_d->document->setGuidesConfig(guides); } + +void KisKraLoader::loadAudio(const KoXmlElement& elem, KisImageSP image) +{ + QDomDocument dom; + KoXml::asQDomElement(dom, elem); + QDomElement qElement =3D dom.firstChildElement(); + + QString fileName; + if (KisDomUtils::loadValue(qElement, "masterChannelPath", &fileName)) { + fileName =3D QDir::toNativeSeparators(fileName); + + QDir baseDirectory =3D QFileInfo(m_d->document->localFilePath()).a= bsoluteDir(); + qDebug() << ppVar(baseDirectory); + + fileName =3D baseDirectory.absoluteFilePath(fileName); + + QFileInfo info(fileName); + + if (!info.exists()) { + m_d->errorMessages << i18n("Audio channel file %1 doesn't exis= t!", fileName); + } else { + image->animationInterface()->setAudioChannelFileName(info.abso= luteFilePath()); + } + } +} diff --git a/plugins/impex/libkra/kis_kra_loader.h b/plugins/impex/libkra/k= is_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 +#include + = using namespace KRA; = @@ -126,6 +129,7 @@ QDomElement KisKraSaver::saveXML(QDomDocument& doc, Ki= sImageSP image) saveAssistantsList(doc, imageElement); saveGrid(doc,imageElement); saveGuides(doc,imageElement); + saveAudio(doc,imageElement); = QDomElement animationElement =3D doc.createElement("animation"); KisDomUtils::saveValue(&animationElement, "framerate", image->animatio= nInterface()->framerate()); @@ -424,3 +428,28 @@ bool KisKraSaver::saveGuides(QDomDocument& doc, QDomEl= ement& element) return true; } = +bool KisKraSaver::saveAudio(QDomDocument& doc, QDomElement& element) +{ + QString fileName =3D m_d->doc->image()->animationInterface()->audioCha= nnelFileName(); + 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 =3D QFileInfo(m_d->doc->localFilePath()).absolu= teDir(); + KIS_ASSERT_RECOVER_RETURN_VALUE(documentDir.exists(), false); + + fileName =3D documentDir.relativeFilePath(fileName); + fileName =3D QDir::fromNativeSeparators(fileName); + + KIS_ASSERT_RECOVER_RETURN_VALUE(!fileName.isEmpty(), false); + + QDomElement audioElement =3D 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/ki= s_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;