File indexing completed on 2024-04-28 04:52:25

0001 /*
0002     SPDX-FileCopyrightText: 2017 Nicolas Carion
0003     SPDX-License-Identifier: GPL-3.0-only OR LicenseRef-KDE-Accepted-GPL
0004 */
0005 #include "clipmodel.hpp"
0006 #include "bin/projectclip.h"
0007 #include "bin/projectitemmodel.h"
0008 #include "clipsnapmodel.hpp"
0009 #include "core.h"
0010 #include "effects/effectstack/model/effectstackmodel.hpp"
0011 #ifdef CRASH_AUTO_TEST
0012 #include "logger.hpp"
0013 #else
0014 #define TRACE_CONSTR(...)
0015 #endif
0016 #include "macros.hpp"
0017 #include "timelinemodel.hpp"
0018 #include "trackmodel.hpp"
0019 #include <QDebug>
0020 #include <effects/effectsrepository.hpp>
0021 #include <mlt++/MltProducer.h>
0022 #include <utility>
0023 
0024 ClipModel::ClipModel(const std::shared_ptr<TimelineModel> &parent, std::shared_ptr<Mlt::Producer> prod, const QString &binClipId, int id,
0025                      PlaylistState::ClipState state, double speed)
0026     : MoveableItem<Mlt::Producer>(parent, id)
0027     , m_producer(std::move(prod))
0028     , m_effectStack(EffectStackModel::construct(m_producer, ObjectId(KdenliveObjectType::TimelineClip, m_id, parent->uuid()), parent->m_undoStack))
0029     , m_clipMarkerModel(new ClipSnapModel())
0030     , m_binClipId(binClipId)
0031     , forceThumbReload(false)
0032     , m_currentState(state)
0033     , m_speed(speed)
0034     , m_fakeTrack(-1)
0035     , m_fakePosition(-1)
0036     , m_positionOffset(0)
0037     , m_subPlaylistIndex(0)
0038     , m_mixDuration(0)
0039     , m_mixCutPos(0)
0040     , m_hasTimeRemap(hasTimeRemap())
0041 {
0042     m_producer->set("kdenlive:id", binClipId.toUtf8().constData());
0043     m_producer->set("_kdenlive_cid", m_id);
0044     std::shared_ptr<ProjectClip> binClip = pCore->projectItemModel()->getClipByBinID(m_binClipId);
0045     m_canBeVideo = binClip->hasVideo();
0046     m_canBeAudio = binClip->hasAudio();
0047     m_clipType = binClip->clipType();
0048     if (binClip) {
0049         m_endlessResize = !binClip->hasLimitedDuration();
0050     } else {
0051         m_endlessResize = false;
0052     }
0053     QObject::connect(m_effectStack.get(), &EffectStackModel::dataChanged, [&](const QModelIndex &, const QModelIndex &, const QVector<int> &roles) {
0054         qDebug() << "// GOT CLIP STACK DATA CHANGE: " << roles;
0055         if (m_currentTrackId != -1) {
0056             if (auto ptr = m_parent.lock()) {
0057                 QModelIndex ix = ptr->makeClipIndexFromID(m_id);
0058                 Q_EMIT ptr->dataChanged(ix, ix, roles);
0059                 qDebug() << "// GOT CLIP STACK DATA CHANGE DONE: " << ix << " = " << roles;
0060             }
0061         }
0062     });
0063 }
0064 
0065 int ClipModel::construct(const std::shared_ptr<TimelineModel> &parent, const QString &binClipId, int id, PlaylistState::ClipState state, int audioStream,
0066                          double speed, bool warp_pitch)
0067 {
0068     id = (id == -1 ? TimelineModel::getNextId() : id);
0069     std::shared_ptr<ProjectClip> binClip = pCore->projectItemModel()->getClipByBinID(binClipId);
0070 
0071     // We refine the state according to what the clip can actually produce
0072     std::pair<bool, bool> videoAudio = stateToBool(state);
0073     videoAudio.first = videoAudio.first && binClip->hasVideo();
0074     videoAudio.second = videoAudio.second && binClip->hasAudio();
0075     state = stateFromBool(videoAudio);
0076     qDebug() << "// GET TIMELINE PROD FOR STREAM: " << audioStream;
0077     std::shared_ptr<Mlt::Producer> cutProducer = binClip->getTimelineProducer(-1, id, state, audioStream, speed);
0078     std::shared_ptr<ClipModel> clip(new ClipModel(parent, cutProducer, binClipId, id, state, speed));
0079     if (!qFuzzyCompare(speed, 1.)) {
0080         cutProducer->parent().set("warp_pitch", warp_pitch ? 1 : 0);
0081     }
0082     qDebug() << "==== BUILT CLIP STREAM: " << clip->audioStream();
0083     TRACE_CONSTR(clip.get(), parent, binClipId, id, state, speed);
0084     clip->setClipState_lambda(state)();
0085     parent->registerClip(clip);
0086     clip->m_clipMarkerModel->setReferenceModel(binClip->getMarkerModel(), speed);
0087     return id;
0088 }
0089 
0090 void ClipModel::allSnaps(std::vector<int> &snaps, int offset) const
0091 {
0092     m_clipMarkerModel->allSnaps(snaps, offset);
0093 }
0094 
0095 int ClipModel::construct(const std::shared_ptr<TimelineModel> &parent, const QString &binClipId, const std::shared_ptr<Mlt::Producer> &producer,
0096                          PlaylistState::ClipState state, int tid, const QString &originalDecimalPoint, int playlist)
0097 {
0098 
0099     // we hand the producer to the bin clip, and in return we get a cut to a good master producer
0100     // We might not be able to use directly the producer that we receive as an argument, because it cannot share the same master producer with any other
0101     // clipModel (due to a mlt limitation, see ProjectClip doc)
0102     int id = TimelineModel::getNextId();
0103     std::shared_ptr<ProjectClip> binClip = pCore->projectItemModel()->getClipByBinID(binClipId);
0104 
0105     // We refine the state according to what the clip can actually produce
0106     std::pair<bool, bool> videoAudio = stateToBool(state);
0107     videoAudio.first = videoAudio.first && binClip->hasVideo();
0108     videoAudio.second = videoAudio.second && binClip->hasAudio();
0109     state = stateFromBool(videoAudio);
0110 
0111     double speed = 1.0;
0112     bool warp_pitch = false;
0113     if (producer->parent().property_exists("warp_speed")) {
0114         speed = producer->parent().get_double("warp_speed");
0115         warp_pitch = producer->parent().get_int("warp_pitch");
0116     }
0117     auto result = binClip->giveMasterAndGetTimelineProducer(id, producer, state, tid, playlist == 1);
0118     std::shared_ptr<ClipModel> clip(new ClipModel(parent, result.first, binClipId, id, state, speed));
0119     if (warp_pitch) {
0120         result.first->parent().set("warp_pitch", 1);
0121     }
0122     clip->setClipState_lambda(state)();
0123     clip->setSubPlaylistIndex(playlist, -1);
0124     parent->registerClip(clip);
0125     if (clip->m_endlessResize) {
0126         // Ensure parent is long enough
0127         if (producer->parent().get_out() < producer->get_length() - 1) {
0128             int out = producer->get_length();
0129             producer->parent().set("length", out + 1);
0130             producer->parent().set("out", out);
0131         }
0132     }
0133     clip->m_effectStack->importEffects(producer, state, result.second, originalDecimalPoint);
0134     clip->m_clipMarkerModel->setReferenceModel(binClip->getMarkerModel(), speed);
0135     return id;
0136 }
0137 
0138 void ClipModel::registerClipToBin(std::shared_ptr<Mlt::Producer> service, bool registerProducer)
0139 {
0140     std::shared_ptr<ProjectClip> binClip = pCore->projectItemModel()->getClipByBinID(m_binClipId);
0141     if (!binClip) {
0142         qDebug() << "Error : Bin clip for id: " << m_binClipId << " NOT AVAILABLE!!!";
0143     }
0144     qDebug() << "REGISTRATION " << m_id << "ptr count" << m_parent.use_count();
0145     binClip->registerService(m_parent, m_id, std::move(service), registerProducer);
0146 }
0147 
0148 void ClipModel::deregisterClipToBin(const QUuid &uuid)
0149 {
0150     std::shared_ptr<ProjectClip> binClip = pCore->projectItemModel()->getClipByBinID(m_binClipId);
0151     binClip->deregisterTimelineClip(m_id, isAudioOnly(), uuid);
0152 }
0153 
0154 ClipModel::~ClipModel() = default;
0155 
0156 bool ClipModel::requestResize(int size, bool right, Fun &undo, Fun &redo, bool logUndo, bool hasMix)
0157 {
0158     QWriteLocker locker(&m_lock);
0159     // qDebug() << "RESIZE CLIP" << m_id << "target size=" << size << "right=" << right << "endless=" << m_endlessResize << "length" <<
0160     // m_producer->get_length()<<" > "<<m_producer->get("kdenlive:duration")<<" = "<<m_producer->get("kdenlive:maxduration");
0161     if (!m_endlessResize && (size <= 0 || size > m_producer->get_length()) && !hasTimeRemap()) {
0162         return false;
0163     }
0164     int delta = getPlaytime() - size;
0165     if (delta == 0) {
0166         return true;
0167     }
0168     int in = m_producer->get_in();
0169     int out = m_producer->get_out();
0170     int oldIn = m_position;
0171     int oldOut = m_position + out - in;
0172     int old_in = in, old_out = out;
0173     // check if there is enough space on the chosen side
0174     if (!m_endlessResize) {
0175         if (!right && in + delta < 0) {
0176             return false;
0177         }
0178         if (right && (out - delta >= m_producer->get_length()) && !hasTimeRemap()) {
0179             return false;
0180         }
0181     }
0182     if (right) {
0183         out -= delta;
0184     } else {
0185         in += delta;
0186     }
0187     // qDebug() << "Resize facts delta =" << delta << "old in" << old_in << "old_out" << old_out << "in" << in << "out" << out;
0188     std::function<bool(void)> track_operation = []() { return true; };
0189     std::function<bool(void)> track_reverse = []() { return true; };
0190     int outPoint = out;
0191     int inPoint = in;
0192     int offset = 0;
0193     int trackDuration = 0;
0194     if (m_endlessResize) {
0195         offset = inPoint;
0196         outPoint = out - in;
0197         inPoint = 0;
0198     }
0199     bool closing = false;
0200     // Ensure producer is long enough
0201     if (m_endlessResize && outPoint > m_producer->parent().get_length()) {
0202         m_producer->parent().set("length", outPoint + 1);
0203         m_producer->parent().set("out", outPoint);
0204         m_producer->set("length", outPoint + 1);
0205     }
0206     if (m_currentTrackId != -1) {
0207         if (auto ptr = m_parent.lock()) {
0208             if (ptr->getTrackById(m_currentTrackId)->isLocked()) {
0209                 return false;
0210             }
0211             closing = ptr->m_closing;
0212             if (right && ptr->getTrackById_const(m_currentTrackId)->isLastClip(getPosition())) {
0213                 trackDuration = ptr->getTrackById_const(m_currentTrackId)->trackDuration();
0214             }
0215             track_operation = ptr->getTrackById(m_currentTrackId)->requestClipResize_lambda(m_id, inPoint, outPoint, right, hasMix, logUndo);
0216         } else {
0217             qDebug() << "Error : Moving clip failed because parent timeline is not available anymore";
0218             Q_ASSERT(false);
0219         }
0220     }
0221     QVector<int> roles{TimelineModel::DurationRole};
0222     if (!right) {
0223         roles.push_back(TimelineModel::StartRole);
0224         roles.push_back(TimelineModel::InPointRole);
0225     } else {
0226         roles.push_back(TimelineModel::OutPointRole);
0227     }
0228     Fun operation = [this, inPoint, outPoint, roles, logUndo, track_operation]() {
0229         if (track_operation()) {
0230             setInOut(inPoint, outPoint);
0231             if (logUndo && !m_endlessResize) {
0232                 Q_EMIT pCore->clipInstanceResized(m_binClipId);
0233             }
0234             return true;
0235         }
0236         return false;
0237     };
0238     Fun postProcess = [this, roles, oldIn, oldOut, right, logUndo]() {
0239         if (m_currentTrackId > -1) {
0240             if (auto ptr = m_parent.lock()) {
0241                 QModelIndex ix = ptr->makeClipIndexFromID(m_id);
0242                 ptr->notifyChange(ix, ix, roles);
0243                 // invalidate timeline preview
0244                 if (logUndo && !ptr->getTrackById_const(m_currentTrackId)->isAudioTrack()) {
0245                     if (right) {
0246                         int newOut = m_position + getOut() - getIn();
0247                         if (oldOut < newOut) {
0248                             Q_EMIT ptr->invalidateZone(oldOut, newOut);
0249                         } else {
0250                             Q_EMIT ptr->invalidateZone(newOut, oldOut);
0251                         }
0252                     } else {
0253                         if (oldIn < m_position) {
0254                             Q_EMIT ptr->invalidateZone(oldIn, m_position);
0255                         } else {
0256                             Q_EMIT ptr->invalidateZone(m_position, oldIn);
0257                         }
0258                     }
0259                 }
0260             }
0261         }
0262         return true;
0263     };
0264     if (operation()) {
0265         // Now, we are in the state in which the timeline should be when we try to revert current action. So we can build the reverse action from here
0266         if (m_currentTrackId != -1) {
0267             if (auto ptr = m_parent.lock()) {
0268                 if (trackDuration > 0 && !closing) {
0269                     // Operation changed parent track duration, update effect stack
0270                     int newDuration = ptr->getTrackById_const(m_currentTrackId)->trackDuration();
0271                     if (logUndo || trackDuration != newDuration) {
0272                         // A clip move changed the track duration, update track effects
0273                         ptr->getTrackById(m_currentTrackId)->m_effectStack->adjustStackLength(true, 0, trackDuration, 0, newDuration, 0, undo, redo, logUndo);
0274                     }
0275                 }
0276                 track_reverse = ptr->getTrackById(m_currentTrackId)->requestClipResize_lambda(m_id, old_in, old_out, right, hasMix, logUndo);
0277             }
0278         }
0279         Fun reverse = [this, old_in, old_out, track_reverse, logUndo, roles]() {
0280             if (track_reverse()) {
0281                 setInOut(old_in, old_out);
0282                 if (logUndo && !m_endlessResize) {
0283                     Q_EMIT pCore->clipInstanceResized(m_binClipId);
0284                 }
0285                 return true;
0286             }
0287             qDebug() << "============\n+++++++++++++++++\nREVRSE TRACK OP FAILED FOR: " << m_id << "\n\n++++++++++++++++";
0288             return false;
0289         };
0290         Fun preProcess = [this, roles, oldIn, oldOut, newIn = m_position, newOut = m_position + getOut() - getIn(), right, logUndo]() {
0291             if (m_currentTrackId > -1) {
0292                 if (auto ptr = m_parent.lock()) {
0293                     QModelIndex ix = ptr->makeClipIndexFromID(m_id);
0294                     ptr->notifyChange(ix, ix, roles);
0295                     // invalidate timeline preview
0296                     if (logUndo && !ptr->getTrackById_const(m_currentTrackId)->isAudioTrack()) {
0297                         if (right) {
0298                             if (oldOut < newOut) {
0299                                 Q_EMIT ptr->invalidateZone(oldOut, newOut);
0300                             } else {
0301                                 Q_EMIT ptr->invalidateZone(newOut, oldOut);
0302                             }
0303                         } else {
0304                             if (oldIn < newIn) {
0305                                 Q_EMIT ptr->invalidateZone(oldIn, newIn);
0306                             } else {
0307                                 Q_EMIT ptr->invalidateZone(newIn, oldIn);
0308                             }
0309                         }
0310                     }
0311                 }
0312             }
0313             return true;
0314         };
0315         if (logUndo) {
0316             qDebug() << "----------\n-----------\n// ADJUSTING EFFECT LENGTH, LOGUNDO " << logUndo << ", " << old_in << "/" << inPoint << "-" << outPoint
0317                      << ", " << m_producer->get_playtime();
0318         }
0319 
0320         if (!closing && logUndo) {
0321             if (hasTimeRemap()) {
0322                 // Add undo /redo ops to resize keyframes
0323                 requestRemapResize(in, out, old_in, old_out, reverse, operation);
0324             }
0325             adjustEffectLength(right, old_in, inPoint, old_out - old_in, m_producer->get_playtime(), offset, reverse, operation, logUndo);
0326         }
0327         postProcess();
0328         PUSH_LAMBDA(postProcess, operation);
0329         PUSH_LAMBDA(preProcess, reverse);
0330         UPDATE_UNDO_REDO(operation, reverse, undo, redo);
0331         return true;
0332     }
0333     return false;
0334 }
0335 
0336 bool ClipModel::requestSlip(int offset, Fun &undo, Fun &redo, bool logUndo)
0337 {
0338     QWriteLocker locker(&m_lock);
0339     if (offset == 0 || m_endlessResize) {
0340         return true;
0341     }
0342     int in = m_producer->get_in();
0343     int out = m_producer->get_out();
0344     int old_in = in, old_out = out;
0345     offset = qBound(out - m_producer->get_length() + 1, offset, in);
0346     int inPoint = in - offset;
0347     int outPoint = out - offset;
0348 
0349     Q_ASSERT(outPoint >= m_producer->get_playtime() - 1);
0350     Q_ASSERT(outPoint < m_producer->get_length());
0351     Q_ASSERT(inPoint >= 0);
0352     Q_ASSERT(inPoint <= m_producer->get_length() - m_producer->get_playtime());
0353     Q_ASSERT(inPoint < outPoint);
0354     Q_ASSERT(out - in == outPoint - inPoint);
0355 
0356     if (m_currentTrackId != -1) {
0357         if (auto ptr = m_parent.lock()) {
0358             if (ptr->getTrackById(m_currentTrackId)->isLocked()) {
0359                 return false;
0360             }
0361         } else {
0362             qDebug() << "Error : Slipping clip failed because parent timeline is not available anymore";
0363             Q_ASSERT(false);
0364         }
0365     }
0366     QVector<int> roles{TimelineModel::StartRole, TimelineModel::InPointRole, TimelineModel::OutPointRole};
0367     Fun operation = [this, inPoint, outPoint, roles, logUndo]() {
0368         setInOut(inPoint, outPoint);
0369         if (m_currentTrackId > -1) {
0370             if (auto ptr = m_parent.lock()) {
0371                 QModelIndex ix = ptr->makeClipIndexFromID(m_id);
0372                 ptr->notifyChange(ix, ix, roles);
0373                 pCore->refreshProjectMonitorOnce();
0374                 // invalidate timeline preview
0375                 if (logUndo && !ptr->getTrackById_const(m_currentTrackId)->isAudioTrack()) {
0376                     Q_EMIT ptr->invalidateZone(m_position, m_position + getPlaytime());
0377                 }
0378             }
0379         }
0380         return true;
0381     };
0382 
0383     qDebug() << "=== SLIP CLIP"
0384              << "pos" << m_position << "offset" << offset << "old_in" << old_in << "old_out" << old_out << "inPoint" << inPoint << "outPoint" << outPoint
0385              << "endless" << m_endlessResize << "playtime" << getPlaytime() << "fulllength" << m_producer->get_length();
0386     ;
0387 
0388     if (operation()) {
0389         Fun reverse = []() { return true; };
0390         // Now, we are in the state in which the timeline should be when we try to revert current action. So we can build the reverse action from here
0391         reverse = [this, old_in, old_out, logUndo, roles]() {
0392             setInOut(old_in, old_out);
0393             if (m_currentTrackId > -1) {
0394                 if (auto ptr = m_parent.lock()) {
0395                     QModelIndex ix = ptr->makeClipIndexFromID(m_id);
0396                     ptr->notifyChange(ix, ix, roles);
0397                     pCore->refreshProjectMonitorOnce();
0398                     if (logUndo && !ptr->getTrackById_const(m_currentTrackId)->isAudioTrack()) {
0399                         Q_EMIT ptr->invalidateZone(m_position, m_position + getPlaytime());
0400                     }
0401                 }
0402             }
0403             return true;
0404         };
0405         qDebug() << "----------\n-----------\n// ADJUSTING EFFECT LENGTH, LOGUNDO " << logUndo << ", " << old_in << "/" << inPoint << ", "
0406                  << m_producer->get_playtime();
0407 
0408         adjustEffectLength(true, old_in, inPoint, old_out - old_in, m_producer->get_playtime(), offset, reverse, operation, logUndo);
0409         UPDATE_UNDO_REDO(operation, reverse, undo, redo);
0410         return true;
0411     }
0412     return false;
0413 }
0414 const QString ClipModel::getProperty(const QString &name) const
0415 {
0416     READ_LOCK();
0417     if (service()->parent().is_valid()) {
0418         return QString::fromUtf8(service()->parent().get(name.toUtf8().constData()));
0419     }
0420     return QString::fromUtf8(service()->get(name.toUtf8().constData()));
0421 }
0422 
0423 int ClipModel::getIntProperty(const QString &name) const
0424 {
0425     READ_LOCK();
0426     if (service()->parent().is_valid()) {
0427         return service()->parent().get_int(name.toUtf8().constData());
0428     }
0429     return service()->get_int(name.toUtf8().constData());
0430 }
0431 
0432 QSize ClipModel::getFrameSize() const
0433 {
0434     READ_LOCK();
0435     std::shared_ptr<ProjectClip> binClip = pCore->projectItemModel()->getClipByBinID(m_binClipId);
0436     if (binClip) {
0437         return binClip->getFrameSize();
0438     }
0439     return QSize();
0440 }
0441 
0442 Mlt::Producer *ClipModel::service() const
0443 {
0444     READ_LOCK();
0445     return m_producer.get();
0446 }
0447 
0448 bool ClipModel::isChain() const
0449 {
0450     READ_LOCK();
0451     return m_producer->parent().type() == mlt_service_chain_type;
0452 }
0453 
0454 bool ClipModel::hasTimeRemap() const
0455 {
0456     READ_LOCK();
0457     if (m_producer->parent().type() == mlt_service_chain_type) {
0458         Mlt::Chain fromChain(m_producer->parent());
0459         int count = fromChain.link_count();
0460         for (int i = 0; i < count; i++) {
0461             QScopedPointer<Mlt::Link> fromLink(fromChain.link(i));
0462             if (fromLink && fromLink->is_valid() && fromLink->property_exists("mlt_service")) {
0463                 if (fromLink->get("mlt_service") == QLatin1String("timeremap")) {
0464                     return true;
0465                 }
0466             }
0467         }
0468     }
0469     return false;
0470 }
0471 
0472 void ClipModel::requestRemapResize(int inPoint, int outPoint, int oldIn, int oldOut, Fun &undo, Fun &redo)
0473 {
0474     Mlt::Chain fromChain(m_producer->parent());
0475     int count = fromChain.link_count();
0476     for (int ix = 0; ix < count; ix++) {
0477         QScopedPointer<Mlt::Link> fromLink(fromChain.link(ix));
0478         if (fromLink && fromLink->is_valid() && fromLink->get("mlt_service")) {
0479             if (fromLink->get("mlt_service") == QLatin1String("timeremap")) {
0480                 // Found a timeremap effect, read params
0481                 std::shared_ptr<Mlt::Link> link = std::make_shared<Mlt::Link>(fromChain.link(ix)->get_link());
0482                 if (!link->property_exists("time_map")) {
0483                     link->set("time_map", fromLink->get("map"));
0484                 }
0485                 (void)link->anim_get_rect("time_map", 0);
0486                 Mlt::Animation anim = link->get_animation("time_map");
0487                 QString oldKfrData = anim.serialize_cut(mlt_time_clock, 0, m_producer->get_length());
0488                 QStringList str = oldKfrData.split(QLatin1Char(';'));
0489                 QMap<int, int> keyframes;
0490                 for (auto &s : str) {
0491                     int pos = m_producer->time_to_frames(s.section(QLatin1Char('='), 0, 0).toUtf8().constData());
0492                     int val = GenTime(s.section(QLatin1Char('='), 1).toDouble()).frames(pCore->getCurrentFps());
0493                     if (s == str.constLast()) {
0494                         // HACK: we always set last keyframe 1 frame after in MLT to ensure we have a correct last frame
0495                         pos--;
0496                     }
0497                     keyframes.insert(pos, val);
0498                 }
0499                 if (keyframes.contains(inPoint) && keyframes.lastKey() == outPoint) {
0500                     // Nothing to do, abort
0501                     return;
0502                 }
0503                 // Adjust start keyframes
0504                 QList<int> toDelete;
0505                 QMap<int, int> toAdd;
0506                 if (inPoint != oldIn && !keyframes.contains(inPoint)) {
0507                     if (inPoint < oldIn) {
0508                         // Move oldIn keyframe to new in
0509                         if (keyframes.contains(oldIn)) {
0510                             int delta = oldIn - inPoint;
0511                             toAdd.insert(inPoint, qMax(0, keyframes.value(oldIn) - delta));
0512                             toDelete << oldIn;
0513                         } else {
0514                             // Move first keyframe available
0515                             bool found = false;
0516                             QMapIterator<int, int> i(keyframes);
0517                             while (i.hasNext()) {
0518                                 i.next();
0519                                 if (i.key() > oldIn) {
0520                                     int delta = i.key() - inPoint;
0521                                     toAdd.insert(inPoint, qMax(0, i.value() - delta));
0522                                     toDelete << i.key();
0523                                     found = true;
0524                                     break;
0525                                 }
0526                             }
0527                             if (!found) {
0528                                 // Add standard keyframe
0529                                 toAdd.insert(inPoint, inPoint);
0530                             }
0531                         }
0532                     } else if (outPoint != oldOut && !keyframes.contains(outPoint)) {
0533                         // inpoint moved forwards, delete previous
0534                         if (keyframes.contains(oldIn)) {
0535                             int delta = inPoint - oldIn;
0536                             toAdd.insert(inPoint, qMax(0, keyframes.value(oldIn) + delta));
0537                         } else {
0538                             toAdd.insert(inPoint, inPoint);
0539                         }
0540                         // Remove all keyframes before
0541                         QMapIterator<int, int> i(keyframes);
0542                         while (i.hasNext()) {
0543                             i.next();
0544                             if (i.key() == 0) {
0545                                 // Don't remove 0 keyframe
0546                                 continue;
0547                             }
0548                             if (i.key() < inPoint) {
0549                                 toDelete << i.key();
0550                             } else {
0551                                 break;
0552                             }
0553                         }
0554                     }
0555                 }
0556                 if (outPoint != oldOut) {
0557                     if (outPoint > oldOut) {
0558                         if (keyframes.contains(oldOut)) {
0559                             int delta = outPoint - oldOut;
0560                             toAdd.insert(outPoint, keyframes.value(oldOut) + delta);
0561                             toDelete << oldOut;
0562                         } else {
0563                             // Add defaut keyframe
0564                             toAdd.insert(outPoint, outPoint);
0565                         }
0566                     } else {
0567                         // Clip reduced
0568                         if (keyframes.contains(oldOut)) {
0569                             int delta = oldOut - outPoint;
0570                             toAdd.insert(outPoint, keyframes.value(oldOut) - delta);
0571                         } else {
0572                             // Add defaut keyframe
0573                             toAdd.insert(outPoint, outPoint);
0574                         }
0575                         // Delete all keyframes after outpoint
0576                         QMapIterator<int, int> i(keyframes);
0577                         while (i.hasNext()) {
0578                             i.next();
0579                             if (i.key() > outPoint) {
0580                                 toDelete << i.key();
0581                             }
0582                         }
0583                     }
0584                 }
0585                 // Remove all requested keyframes
0586                 for (int d : qAsConst(toDelete)) {
0587                     keyframes.remove(d);
0588                 }
0589                 // Add replacement keyframes
0590                 QMapIterator<int, int> i(toAdd);
0591                 while (i.hasNext()) {
0592                     i.next();
0593                     keyframes.insert(i.key(), i.value());
0594                 }
0595                 QStringList result;
0596                 QMapIterator<int, int> j(keyframes);
0597                 int offset = 0;
0598                 while (j.hasNext()) {
0599                     j.next();
0600                     if (j.key() == keyframes.lastKey()) {
0601                         // HACK: we always set last keyframe 1 frame after in MLT to ensure we have a correct last frame
0602                         offset = 1;
0603                     }
0604                     result << QString("%1=%2")
0605                                   .arg(m_producer->frames_to_time(j.key() + offset, mlt_time_clock))
0606                                   .arg(GenTime(j.value(), pCore->getCurrentFps()).seconds());
0607                 }
0608                 Fun operation = [this, kfrData = result.join(QLatin1Char(';'))]() {
0609                     setRemapValue("time_map", kfrData.toUtf8().constData());
0610                     if (auto ptr = m_parent.lock()) {
0611                         QModelIndex ix = ptr->makeClipIndexFromID(m_id);
0612                         ptr->notifyChange(ix, ix, TimelineModel::FinalMoveRole);
0613                     }
0614                     return true;
0615                 };
0616                 Fun reverse = [this, oldKfrData]() {
0617                     setRemapValue("time_map", oldKfrData.toUtf8().constData());
0618                     if (auto ptr = m_parent.lock()) {
0619                         QModelIndex ix = ptr->makeClipIndexFromID(m_id);
0620                         ptr->notifyChange(ix, ix, TimelineModel::FinalMoveRole);
0621                     }
0622                     return true;
0623                 };
0624                 operation();
0625                 PUSH_LAMBDA(operation, redo);
0626                 PUSH_FRONT_LAMBDA(reverse, undo);
0627             }
0628         }
0629     }
0630 }
0631 
0632 int ClipModel::getRemapInputDuration() const
0633 {
0634     Mlt::Chain fromChain(m_producer->parent());
0635     int count = fromChain.link_count();
0636     for (int i = 0; i < count; i++) {
0637         QScopedPointer<Mlt::Link> fromLink(fromChain.link(i));
0638         if (fromLink && fromLink->is_valid() && fromLink->get("mlt_service")) {
0639             if (fromLink->get("mlt_service") == QLatin1String("timeremap")) {
0640                 // Found a timeremap effect, read params
0641                 std::shared_ptr<Mlt::Link> link = std::make_shared<Mlt::Link>(fromChain.link(i)->get_link());
0642                 if (!link->property_exists("time_map")) {
0643                     link->set("time_map", fromLink->get("map"));
0644                 }
0645                 QString mapData = link->get("time_map");
0646                 int min = GenTime(link->anim_get_double("time_map", getIn())).frames(pCore->getCurrentFps());
0647                 QStringList str = mapData.split(QLatin1Char(';'));
0648                 int max = -1;
0649                 for (auto &s : str) {
0650                     int val = GenTime(s.section(QLatin1Char('='), 1).toDouble()).frames(pCore->getCurrentFps());
0651                     if (val > max) {
0652                         max = val;
0653                     }
0654                 }
0655                 return max - min;
0656             }
0657         }
0658     }
0659     return 0;
0660 }
0661 
0662 void ClipModel::setRemapValue(const QString &name, const QString &value)
0663 {
0664     if (m_producer->parent().type() != mlt_service_chain_type) {
0665         return;
0666     }
0667     Mlt::Chain fromChain(m_producer->parent());
0668     int count = fromChain.link_count();
0669     for (int i = 0; i < count; i++) {
0670         QScopedPointer<Mlt::Link> fromLink(fromChain.link(i));
0671         if (fromLink && fromLink->is_valid() && fromLink->get("mlt_service")) {
0672             if (fromLink->get("mlt_service") == QLatin1String("timeremap")) {
0673                 // Found a timeremap effect, read params
0674                 std::shared_ptr<Mlt::Link> link = std::make_shared<Mlt::Link>(fromChain.link(i)->get_link());
0675                 link->set(name.toUtf8().constData(), value.toUtf8().constData());
0676                 return;
0677             }
0678         }
0679     }
0680 }
0681 
0682 QMap<QString, QString> ClipModel::getRemapValues() const
0683 {
0684     QMap<QString, QString> result;
0685     if (m_producer->parent().type() != mlt_service_chain_type) {
0686         return result;
0687     }
0688     Mlt::Chain fromChain(m_producer->parent());
0689     int count = fromChain.link_count();
0690     for (int i = 0; i < count; i++) {
0691         QScopedPointer<Mlt::Link> fromLink(fromChain.link(i));
0692         if (fromLink && fromLink->is_valid() && fromLink->get("mlt_service")) {
0693             if (fromLink->get("mlt_service") == QLatin1String("timeremap")) {
0694                 // Found a timeremap effect, read params
0695                 std::shared_ptr<Mlt::Link> link = std::make_shared<Mlt::Link>(fromChain.link(i)->get_link());
0696                 // Ensure animation uses time not frames
0697                 if (!link->property_exists("time_map")) {
0698                     link->set("time_map", link->get("map"));
0699                 }
0700                 (void)link->anim_get_double("time_map", 0);
0701                 Mlt::Animation anim = link->get_animation("time_map");
0702                 result.insert(QStringLiteral("time_map"), anim.serialize_cut(mlt_time_clock, 0, m_producer->get_length()));
0703                 result.insert(QStringLiteral("pitch"), link->get("pitch"));
0704                 result.insert(QStringLiteral("image_mode"), link->get("image_mode"));
0705                 break;
0706             }
0707         }
0708     }
0709     return result;
0710 }
0711 
0712 std::shared_ptr<Mlt::Producer> ClipModel::getProducer()
0713 {
0714     READ_LOCK();
0715     return m_producer;
0716 }
0717 
0718 int ClipModel::getPlaytime() const
0719 {
0720     READ_LOCK();
0721     return m_producer->get_playtime();
0722 }
0723 
0724 void ClipModel::setTimelineEffectsEnabled(bool enabled)
0725 {
0726     QWriteLocker locker(&m_lock);
0727     m_effectStack->setEffectStackEnabled(enabled);
0728 }
0729 
0730 bool ClipModel::addEffect(const QString &effectId)
0731 {
0732     QWriteLocker locker(&m_lock);
0733     if (EffectsRepository::get()->isAudioEffect(effectId)) {
0734         if (m_currentState == PlaylistState::VideoOnly) {
0735             return false;
0736         }
0737     } else if (m_currentState == PlaylistState::AudioOnly) {
0738         return false;
0739     }
0740     if (EffectsRepository::get()->isTextEffect(effectId) && m_clipType != ClipType::Text) {
0741         return false;
0742     }
0743     m_effectStack->appendEffect(effectId, true);
0744     return true;
0745 }
0746 
0747 bool ClipModel::addEffectWithUndo(const QString &effectId, Fun &undo, Fun &redo)
0748 {
0749     QWriteLocker locker(&m_lock);
0750     if (EffectsRepository::get()->isAudioEffect(effectId)) {
0751         if (m_currentState == PlaylistState::VideoOnly) {
0752             return false;
0753         }
0754     } else if (m_currentState == PlaylistState::AudioOnly) {
0755         return false;
0756     }
0757     if (EffectsRepository::get()->isTextEffect(effectId) && m_clipType != ClipType::Text) {
0758         return false;
0759     }
0760     return m_effectStack->appendEffectWithUndo(effectId, undo, redo);
0761 }
0762 
0763 bool ClipModel::copyEffect(const QUuid &uuid, const std::shared_ptr<EffectStackModel> &stackModel, int rowId)
0764 {
0765     QWriteLocker locker(&m_lock);
0766     QDomDocument doc;
0767     m_effectStack->copyXmlEffect(stackModel->rowToXml(uuid, rowId, doc));
0768     return true;
0769 }
0770 
0771 bool ClipModel::copyEffectWithUndo(const QUuid &uuid, const std::shared_ptr<EffectStackModel> &stackModel, int rowId, Fun &undo, Fun &redo)
0772 {
0773     QWriteLocker locker(&m_lock);
0774     QDomDocument doc;
0775     m_effectStack->copyXmlEffectWithUndo(stackModel->rowToXml(uuid, rowId, doc), undo, redo);
0776     return true;
0777 }
0778 
0779 bool ClipModel::importEffects(std::shared_ptr<EffectStackModel> stackModel)
0780 {
0781     QWriteLocker locker(&m_lock);
0782     m_effectStack->importEffects(std::move(stackModel), m_currentState);
0783     return true;
0784 }
0785 
0786 bool ClipModel::importEffects(std::weak_ptr<Mlt::Service> service)
0787 {
0788     QWriteLocker locker(&m_lock);
0789     m_effectStack->importEffects(std::move(service), m_currentState);
0790     return true;
0791 }
0792 
0793 bool ClipModel::removeFade(bool fromStart)
0794 {
0795     QWriteLocker locker(&m_lock);
0796     m_effectStack->removeFade(fromStart);
0797     return true;
0798 }
0799 
0800 bool ClipModel::adjustEffectLength(bool adjustFromEnd, int oldIn, int newIn, int oldDuration, int duration, int offset, Fun &undo, Fun &redo, bool logUndo)
0801 {
0802     QWriteLocker locker(&m_lock);
0803     return m_effectStack->adjustStackLength(adjustFromEnd, oldIn, oldDuration, newIn, duration, offset, undo, redo, logUndo);
0804 }
0805 
0806 bool ClipModel::adjustEffectLength(const QString &effectName, int duration, int originalDuration, Fun &undo, Fun &redo)
0807 {
0808     QWriteLocker locker(&m_lock);
0809     Fun operation = [this, duration, effectName, originalDuration]() {
0810         return m_effectStack->adjustFadeLength(duration, effectName.startsWith(QLatin1String("fadein")) || effectName.startsWith(QLatin1String("fade_to_")),
0811                                                audioEnabled(), !isAudioOnly(), originalDuration > 0);
0812     };
0813     if (operation() && originalDuration > 0) {
0814         Fun reverse = [this, originalDuration, effectName]() {
0815             return m_effectStack->adjustFadeLength(originalDuration,
0816                                                    effectName.startsWith(QLatin1String("fadein")) || effectName.startsWith(QLatin1String("fade_to_")),
0817                                                    audioEnabled(), !isAudioOnly(), true);
0818         };
0819         UPDATE_UNDO_REDO(operation, reverse, undo, redo);
0820     }
0821     return true;
0822 }
0823 
0824 bool ClipModel::audioEnabled() const
0825 {
0826     READ_LOCK();
0827     return stateToBool(m_currentState).second;
0828 }
0829 
0830 bool ClipModel::isAudioOnly() const
0831 {
0832     READ_LOCK();
0833     return m_currentState == PlaylistState::AudioOnly;
0834 }
0835 
0836 void ClipModel::refreshProducerFromBin(int trackId, PlaylistState::ClipState state, int stream, double speed, bool hasPitch, bool secondPlaylist,
0837                                        bool timeremap)
0838 {
0839     // We require that the producer is not in the track when we refresh the producer, because otherwise the modification will not be propagated. Remove the clip
0840     // first, refresh, and then replant.
0841     QWriteLocker locker(&m_lock);
0842     int in = getIn();
0843     int out = getOut();
0844     if (!qFuzzyCompare(speed, m_speed) && !qFuzzyIsNull(speed)) {
0845         in = int(in * std::abs(m_speed / speed));
0846         out = in + getPlaytime() - 1;
0847         // prevent going out of the clip's range
0848         out = std::min(out, int(double(m_producer->get_length()) * std::abs(m_speed / speed)) - 1);
0849         m_speed = speed;
0850         qDebug() << "changing speed" << in << out << m_speed;
0851     }
0852     QString remapMap;
0853     int remapPitch = 0;
0854     QString remapBlend;
0855     if (m_hasTimeRemap) {
0856         if (m_producer->parent().type() == mlt_service_chain_type) {
0857             Mlt::Chain fromChain(m_producer->parent());
0858             int count = fromChain.link_count();
0859             for (int i = 0; i < count; i++) {
0860                 QScopedPointer<Mlt::Link> fromLink(fromChain.link(i));
0861                 if (fromLink && fromLink->is_valid() && fromLink->get("mlt_service")) {
0862                     if (fromLink->get("mlt_service") == QLatin1String("timeremap")) {
0863                         // Found a timeremap effect, read params
0864                         if (!fromLink->property_exists("time_map")) {
0865                             fromLink->set("time_map", fromLink->get("map"));
0866                         }
0867                         remapMap = fromLink->get("time_map");
0868                         remapPitch = fromLink->get_int("pitch");
0869                         remapBlend = fromLink->get("image_mode");
0870                         break;
0871                     }
0872                 }
0873             }
0874         } else {
0875             qDebug() << "=== NON CHAIN ON REFRESH!!!";
0876         }
0877     }
0878     ProjectClip::TimeWarpInfo remapInfo;
0879     remapInfo.enableRemap = timeremap;
0880     if (timeremap) {
0881         remapInfo.timeMapData = remapMap;
0882         remapInfo.pitchShift = remapPitch;
0883         remapInfo.imageMode = remapBlend;
0884     }
0885 
0886     std::shared_ptr<ProjectClip> binClip = pCore->projectItemModel()->getClipByBinID(m_binClipId);
0887     std::shared_ptr<Mlt::Producer> binProducer = binClip->getTimelineProducer(trackId, m_id, state, stream, m_speed, secondPlaylist, remapInfo);
0888     m_producer = std::move(binProducer);
0889     m_producer->set_in_and_out(in, out);
0890     if (m_hasTimeRemap != hasTimeRemap()) {
0891         m_hasTimeRemap = !m_hasTimeRemap;
0892         // producer is not on a track, no data refresh needed
0893     }
0894     if (m_hasTimeRemap) {
0895         // Restore timeremap parameters
0896         if (m_producer->parent().type() == mlt_service_chain_type) {
0897             Mlt::Chain fromChain(m_producer->parent());
0898             int count = fromChain.link_count();
0899             for (int i = 0; i < count; i++) {
0900                 QScopedPointer<Mlt::Link> fromLink(fromChain.link(i));
0901                 if (fromLink && fromLink->is_valid() && fromLink->property_exists("mlt_service")) {
0902                     if (fromLink->get("mlt_service") == QLatin1String("timeremap")) {
0903                         // Found a timeremap effect, read params
0904                         fromLink->set("time_map", remapMap.toUtf8().constData());
0905                         fromLink->set("pitch", remapPitch);
0906                         fromLink->set("image_mode", remapBlend.toUtf8().constData());
0907                         break;
0908                     }
0909                 }
0910             }
0911         }
0912     }
0913     if (hasPitch) {
0914         // Check if pitch shift is enabled
0915         m_producer->parent().set("warp_pitch", 1);
0916     } else if (!qFuzzyCompare(m_speed, 1.)) {
0917         m_producer->parent().set("warp_pitch", 0);
0918     }
0919     // replant effect stack in updated service
0920     int activeEffect = m_effectStack->getActiveEffect();
0921     m_effectStack->resetService(m_producer);
0922     m_producer->set("kdenlive:id", binClip->clipId().toUtf8().constData());
0923     m_producer->set("_kdenlive_cid", m_id);
0924     if (activeEffect > 0) {
0925         m_producer->set("kdenlive:activeeffect", activeEffect);
0926     }
0927     m_endlessResize = !binClip->hasLimitedDuration();
0928 }
0929 
0930 void ClipModel::refreshProducerFromBin(int trackId)
0931 {
0932     if (trackId == -1) {
0933         trackId = m_currentTrackId;
0934     }
0935     bool hasPitch = false;
0936     if (!qFuzzyCompare(getSpeed(), 1.)) {
0937         hasPitch = m_producer->parent().get_int("warp_pitch") == 1;
0938     }
0939     int stream = m_producer->parent().get_int("audio_index");
0940     refreshProducerFromBin(trackId, m_currentState, stream, 0, hasPitch, m_subPlaylistIndex == 1, hasTimeRemap());
0941 }
0942 
0943 bool ClipModel::useTimeRemapProducer(bool enable, Fun &undo, Fun &redo)
0944 {
0945     if (m_endlessResize) {
0946         // no timewarp for endless producers
0947         return false;
0948     }
0949     std::function<bool(void)> local_undo = []() { return true; };
0950     std::function<bool(void)> local_redo = []() { return true; };
0951     int audioStream = getIntProperty(QStringLiteral("audio_index"));
0952     QMap<QString, QString> remapProperties;
0953     remapProperties.insert(QStringLiteral("image_mode"), QStringLiteral("nearest"));
0954     if (!enable) {
0955         // Store the remap properties
0956         if (m_producer->parent().type() == mlt_service_chain_type) {
0957             Mlt::Chain fromChain(m_producer->parent());
0958             int count = fromChain.link_count();
0959             for (int i = 0; i < count; i++) {
0960                 QScopedPointer<Mlt::Link> fromLink(fromChain.link(i));
0961                 if (fromLink && fromLink->is_valid() && fromLink->get("mlt_service")) {
0962                     if (fromLink->get("mlt_service") == QLatin1String("timeremap")) {
0963                         // Found a timeremap effect, read params
0964                         remapProperties.insert(QStringLiteral("time_map"), fromLink->get("time_map"));
0965                         remapProperties.insert(QStringLiteral("pitch"), fromLink->get("pitch"));
0966                         remapProperties.insert(QStringLiteral("image_mode"), fromLink->get("image_mode"));
0967                         break;
0968                     }
0969                 }
0970             }
0971         } else {
0972             qDebug() << "=== NON CHAIN ON REFRESH!!!";
0973         }
0974     }
0975     auto operation = useTimeRemapProducer_lambda(enable, audioStream, remapProperties);
0976     auto reverse = useTimeRemapProducer_lambda(!enable, audioStream, remapProperties);
0977     if (operation()) {
0978         UPDATE_UNDO_REDO(operation, reverse, local_undo, local_redo);
0979         UPDATE_UNDO_REDO(local_redo, local_undo, undo, redo);
0980         return true;
0981     }
0982     return false;
0983 }
0984 
0985 Fun ClipModel::useTimeRemapProducer_lambda(bool enable, int audioStream, const QMap<QString, QString> &remapProperties)
0986 {
0987     QWriteLocker locker(&m_lock);
0988     return [enable, audioStream, remapProperties, this]() {
0989         refreshProducerFromBin(m_currentTrackId, m_currentState, audioStream, 0, false, false, enable);
0990         if (enable) {
0991             QMapIterator<QString, QString> j(remapProperties);
0992             if (m_producer->parent().type() == mlt_service_chain_type) {
0993                 Mlt::Chain fromChain(m_producer->parent());
0994                 int count = fromChain.link_count();
0995                 for (int i = 0; i < count; i++) {
0996                     QScopedPointer<Mlt::Link> fromLink(fromChain.link(i));
0997                     if (fromLink && fromLink->is_valid() && fromLink->get("mlt_service")) {
0998                         if (fromLink->get("mlt_service") == QLatin1String("timeremap")) {
0999                             while (j.hasNext()) {
1000                                 j.next();
1001                                 fromLink->set(j.key().toUtf8().constData(), j.value().toUtf8().constData());
1002                             }
1003                             break;
1004                         }
1005                     }
1006                 }
1007             }
1008         }
1009         return true;
1010     };
1011 }
1012 
1013 bool ClipModel::useTimewarpProducer(double speed, bool pitchCompensate, bool changeDuration, Fun &undo, Fun &redo)
1014 {
1015     if (m_endlessResize) {
1016         // no timewarp for endless producers
1017         return false;
1018     }
1019     std::function<bool(void)> local_undo = []() { return true; };
1020     std::function<bool(void)> local_redo = []() { return true; };
1021     double previousSpeed = getSpeed();
1022     int oldDuration = getPlaytime();
1023     int newDuration = qRound(oldDuration * std::fabs(m_speed / speed));
1024     int oldOut = getOut();
1025     int oldIn = getIn();
1026     bool revertSpeed = false;
1027     if (speed < 0) {
1028         if (previousSpeed > 0) {
1029             revertSpeed = true;
1030         }
1031     } else if (previousSpeed < 0) {
1032         revertSpeed = true;
1033     }
1034     bool hasPitch = getIntProperty(QStringLiteral("warp_pitch"));
1035     int audioStream = getIntProperty(QStringLiteral("audio_index"));
1036     auto operation = useTimewarpProducer_lambda(speed, audioStream, pitchCompensate);
1037     auto reverse = useTimewarpProducer_lambda(previousSpeed, audioStream, hasPitch);
1038     if (revertSpeed || (changeDuration && oldOut >= newDuration)) {
1039         // in that case, we are going to shrink the clip when changing the producer. We must undo that when reloading the old producer
1040         reverse = [reverse, oldIn, oldOut, this]() {
1041             bool res = reverse();
1042             if (res) {
1043                 setInOut(oldIn, oldOut);
1044             }
1045             return res;
1046         };
1047     }
1048     if (revertSpeed) {
1049         int out = getOut() + 1;
1050         int in = qMax(0, qRound((m_producer->get_length() - 1 - out) * std::fabs(m_speed / speed)));
1051         out = in + newDuration;
1052         operation = [operation, in, out, this]() {
1053             bool res = operation();
1054             if (res) {
1055                 setInOut(in, out);
1056             } else {
1057             }
1058             return res;
1059         };
1060     }
1061     if (operation()) {
1062         UPDATE_UNDO_REDO(operation, reverse, local_undo, local_redo);
1063         // When calculating duration, result can be a few frames longer than possible duration so adjust
1064         if (changeDuration) {
1065             int requestedDuration = qMin(newDuration, getMaxDuration() - getIn());
1066             if (requestedDuration != getPlaytime()) {
1067                 bool res = requestResize(requestedDuration, true, local_undo, local_redo, true);
1068                 if (!res) {
1069                     qDebug() << "==== CLIP WARP UPDATE DURATION FAILED!!!!";
1070                     local_undo();
1071                     return false;
1072                 }
1073             }
1074         }
1075         adjustEffectLength(false, oldIn, getIn(), oldOut - oldIn, m_producer->get_playtime(), 0, local_undo, local_redo, true);
1076         UPDATE_UNDO_REDO(local_redo, local_undo, undo, redo);
1077         return true;
1078     }
1079     qDebug() << "tw: operation fail";
1080     return false;
1081 }
1082 
1083 Fun ClipModel::useTimewarpProducer_lambda(double speed, int stream, bool pitchCompensate)
1084 {
1085     QWriteLocker locker(&m_lock);
1086     return [speed, stream, pitchCompensate, this]() {
1087         qDebug() << "timeWarp producer" << speed;
1088         refreshProducerFromBin(m_currentTrackId, m_currentState, stream, speed, pitchCompensate);
1089         return true;
1090     };
1091 }
1092 
1093 const QString &ClipModel::binId() const
1094 {
1095     return m_binClipId;
1096 }
1097 
1098 std::shared_ptr<MarkerListModel> ClipModel::getMarkerModel() const
1099 {
1100     READ_LOCK();
1101     return pCore->projectItemModel()->getClipByBinID(m_binClipId)->getMarkerModel();
1102 }
1103 
1104 int ClipModel::audioChannels() const
1105 {
1106     READ_LOCK();
1107     return pCore->projectItemModel()->getClipByBinID(m_binClipId)->audioChannels();
1108 }
1109 
1110 bool ClipModel::audioMultiStream() const
1111 {
1112     READ_LOCK();
1113     return pCore->projectItemModel()->getClipByBinID(m_binClipId)->audioStreamsCount() > 1;
1114 }
1115 
1116 int ClipModel::audioStream() const
1117 {
1118     return m_producer->parent().get_int("audio_index");
1119 }
1120 
1121 int ClipModel::audioStreamIndex() const
1122 {
1123     READ_LOCK();
1124     return pCore->projectItemModel()->getClipByBinID(m_binClipId)->audioStreamIndex(m_producer->parent().get_int("audio_index")) + 1;
1125 }
1126 
1127 int ClipModel::fadeIn() const
1128 {
1129     return m_effectStack->getFadePosition(true);
1130 }
1131 
1132 int ClipModel::fadeOut() const
1133 {
1134     return m_effectStack->getFadePosition(false);
1135 }
1136 
1137 double ClipModel::getSpeed() const
1138 {
1139     return m_speed;
1140 }
1141 
1142 KeyframeModel *ClipModel::getKeyframeModel()
1143 {
1144     return m_effectStack->getEffectKeyframeModel();
1145 }
1146 
1147 bool ClipModel::showKeyframes() const
1148 {
1149     READ_LOCK();
1150     return !service()->get_int("kdenlive:hide_keyframes");
1151 }
1152 
1153 void ClipModel::setShowKeyframes(bool show)
1154 {
1155     QWriteLocker locker(&m_lock);
1156     service()->set("kdenlive:hide_keyframes", !show);
1157 }
1158 
1159 void ClipModel::setPosition(int pos)
1160 {
1161     MoveableItem::setPosition(pos);
1162     m_clipMarkerModel->updateSnapModelPos(pos);
1163 }
1164 
1165 void ClipModel::setMixDuration(int mix, int cutOffset)
1166 {
1167     if (mix == 0) {
1168         // Deleting a mix
1169         m_mixCutPos = 0;
1170     } else {
1171         // Creating a new mix
1172         m_mixCutPos = cutOffset;
1173     }
1174     m_mixDuration = mix;
1175     if (m_mixCutPos > 0) {
1176         m_clipMarkerModel->updateSnapMixPosition(m_mixDuration - m_mixCutPos);
1177     }
1178 }
1179 
1180 void ClipModel::setMixDuration(int mix)
1181 {
1182     m_mixDuration = mix;
1183     if (m_mixDuration == 0) {
1184         m_mixCutPos = 0;
1185     }
1186     m_clipMarkerModel->updateSnapMixPosition(m_mixDuration - m_mixCutPos);
1187 }
1188 
1189 int ClipModel::getMixDuration() const
1190 {
1191     return m_mixDuration;
1192 }
1193 
1194 int ClipModel::getMixCutPosition() const
1195 {
1196     return m_mixCutPos;
1197 }
1198 
1199 void ClipModel::setInOut(int in, int out)
1200 {
1201     MoveableItem::setInOut(in, out);
1202     m_clipMarkerModel->updateSnapModelInOut({in, out, qMax(0, m_mixDuration - m_mixCutPos)});
1203 }
1204 
1205 void ClipModel::setCurrentTrackId(int tid, bool finalMove)
1206 {
1207     if (tid == m_currentTrackId) {
1208         return;
1209     }
1210     bool registerSnap = m_currentTrackId == -1 && tid > -1;
1211 
1212     if (m_currentTrackId > -1 && tid == -1) {
1213         // Removing clip
1214         m_clipMarkerModel->deregisterSnapModel();
1215     }
1216     MoveableItem::setCurrentTrackId(tid, finalMove);
1217     if (registerSnap) {
1218         if (auto ptr = m_parent.lock()) {
1219             m_clipMarkerModel->registerSnapModel(ptr->m_snaps, getPosition(), getIn(), getOut(), m_speed);
1220         }
1221     }
1222 
1223     if (finalMove && m_lastTrackId != m_currentTrackId) {
1224         if (tid != -1) {
1225             refreshProducerFromBin(m_currentTrackId);
1226         }
1227         m_lastTrackId = m_currentTrackId;
1228     }
1229 }
1230 
1231 Fun ClipModel::setClipState_lambda(PlaylistState::ClipState state)
1232 {
1233     QWriteLocker locker(&m_lock);
1234     return [this, state]() {
1235         if (auto ptr = m_parent.lock()) {
1236             m_currentState = state;
1237             // Enforce producer reload
1238             m_lastTrackId = -1;
1239             if (m_currentTrackId != -1 && ptr->isClip(m_id)) { // if this is false, the clip is being created. Don't update model in that case
1240                 refreshProducerFromBin(m_currentTrackId);
1241                 QModelIndex ix = ptr->makeClipIndexFromID(m_id);
1242                 Q_EMIT ptr->dataChanged(ix, ix, {TimelineModel::StatusRole});
1243             }
1244             return true;
1245         }
1246         return false;
1247     };
1248 }
1249 
1250 bool ClipModel::setClipState(PlaylistState::ClipState state, Fun &undo, Fun &redo)
1251 {
1252     if (state == PlaylistState::VideoOnly && !canBeVideo()) {
1253         return false;
1254     }
1255     if (state == PlaylistState::AudioOnly && !canBeAudio()) {
1256         return false;
1257     }
1258     if (state == m_currentState) {
1259         return true;
1260     }
1261     auto old_state = m_currentState;
1262     auto operation = setClipState_lambda(state);
1263     if (operation()) {
1264         auto reverse = setClipState_lambda(old_state);
1265         UPDATE_UNDO_REDO(operation, reverse, undo, redo);
1266         return true;
1267     }
1268     return false;
1269 }
1270 
1271 PlaylistState::ClipState ClipModel::clipState() const
1272 {
1273     READ_LOCK();
1274     return m_currentState;
1275 }
1276 
1277 ClipType::ProducerType ClipModel::clipType() const
1278 {
1279     READ_LOCK();
1280     return m_clipType;
1281 }
1282 
1283 void ClipModel::passTimelineProperties(const std::shared_ptr<ClipModel> &other)
1284 {
1285     READ_LOCK();
1286     Mlt::Properties source(m_producer->get_properties());
1287     Mlt::Properties dest(other->service()->get_properties());
1288     dest.pass_list(source, "kdenlive:hide_keyframes,kdenlive:activeeffect");
1289 }
1290 
1291 bool ClipModel::canBeVideo() const
1292 {
1293     return m_canBeVideo;
1294 }
1295 
1296 bool ClipModel::canBeAudio() const
1297 {
1298     return m_canBeAudio;
1299 }
1300 
1301 const QString ClipModel::effectNames() const
1302 {
1303     READ_LOCK();
1304     return m_effectStack->effectNames();
1305 }
1306 
1307 bool ClipModel::stackEnabled() const
1308 {
1309     READ_LOCK();
1310     return m_effectStack->isStackEnabled();
1311 }
1312 
1313 const QStringList ClipModel::externalFiles() const
1314 {
1315     READ_LOCK();
1316     return m_effectStack->externalFiles();
1317 }
1318 
1319 int ClipModel::getFakeTrackId() const
1320 {
1321     return m_fakeTrack;
1322 }
1323 
1324 void ClipModel::setFakeTrackId(int fid)
1325 {
1326     m_fakeTrack = fid;
1327 }
1328 
1329 int ClipModel::getFakePosition() const
1330 {
1331     return m_fakePosition;
1332 }
1333 
1334 void ClipModel::setFakePosition(int fpos)
1335 {
1336     m_fakePosition = fpos;
1337 }
1338 
1339 QDomElement ClipModel::toXml(QDomDocument &document)
1340 {
1341     QDomElement container = document.createElement(QStringLiteral("clip"));
1342     container.setAttribute(QStringLiteral("binid"), m_binClipId);
1343     container.setAttribute(QStringLiteral("id"), m_id);
1344     container.setAttribute(QStringLiteral("in"), getIn());
1345     container.setAttribute(QStringLiteral("out"), getOut());
1346     container.setAttribute(QStringLiteral("position"), getPosition());
1347     container.setAttribute(QStringLiteral("state"), m_currentState);
1348     container.setAttribute(QStringLiteral("playlist"), m_subPlaylistIndex);
1349     if (auto ptr = m_parent.lock()) {
1350         int trackId = ptr->getTrackPosition(m_currentTrackId);
1351         container.setAttribute(QStringLiteral("track"), trackId);
1352         if (ptr->isAudioTrack(getCurrentTrackId())) {
1353             container.setAttribute(QStringLiteral("audioTrack"), 1);
1354             int partner = ptr->getClipSplitPartner(m_id);
1355             if (partner != -1) {
1356                 int mirrorId = ptr->getMirrorVideoTrackId(m_currentTrackId);
1357                 if (mirrorId > -1) {
1358                     mirrorId = ptr->getTrackPosition(mirrorId);
1359                 }
1360                 container.setAttribute(QStringLiteral("mirrorTrack"), mirrorId);
1361             } else {
1362                 container.setAttribute(QStringLiteral("mirrorTrack"), QStringLiteral("-1"));
1363             }
1364         }
1365     }
1366     container.setAttribute(QStringLiteral("speed"), QString::number(m_speed, 'f'));
1367     container.setAttribute(QStringLiteral("audioStream"), getIntProperty(QStringLiteral("audio_index")));
1368     if (!qFuzzyCompare(m_speed, 1.)) {
1369         container.setAttribute(QStringLiteral("warp_pitch"), getIntProperty(QStringLiteral("warp_pitch")));
1370     }
1371     if (m_hasTimeRemap) {
1372         if (m_producer->parent().type() == mlt_service_chain_type) {
1373             Mlt::Chain fromChain(m_producer->parent());
1374             int count = fromChain.link_count();
1375             for (int i = 0; i < count; i++) {
1376                 QScopedPointer<Mlt::Link> fromLink(fromChain.link(i));
1377                 if (fromLink && fromLink->is_valid() && fromLink->get("mlt_service")) {
1378                     if (fromLink->get("mlt_service") == QLatin1String("timeremap")) {
1379                         // Found a timeremap effect, read params
1380                         container.setAttribute(QStringLiteral("timemap"), fromLink->get("time_map"));
1381                         container.setAttribute(QStringLiteral("timepitch"), fromLink->get_int("pitch"));
1382                         container.setAttribute(QStringLiteral("timeblend"), fromLink->get("image_mode"));
1383                         break;
1384                     }
1385                 }
1386             }
1387         } else {
1388             qDebug() << "=== NON CHAIN ON REFRESH!!!";
1389         }
1390     }
1391     container.appendChild(m_effectStack->toXml(document));
1392     return container;
1393 }
1394 
1395 bool ClipModel::checkConsistency()
1396 {
1397     if (!m_effectStack->checkConsistency()) {
1398         qDebug() << "Consistency check failed for effectstack";
1399         return false;
1400     }
1401     if (m_currentTrackId == -1) {
1402         qDebug() << ":::: CLIP IS NOT INSERTED IN A TRACK";
1403         return true;
1404     }
1405     std::shared_ptr<ProjectClip> binClip = pCore->projectItemModel()->getClipByBinID(m_binClipId);
1406     const QUuid timelineUuid = getUuid();
1407     auto instances = binClip->timelineInstances(timelineUuid);
1408     bool found = instances.contains(m_id);
1409     if (!found) {
1410         qDebug() << "ERROR: binClip doesn't acknowledge timeline clip existence: " << m_id << ", CURRENT TRACK: " << m_currentTrackId;
1411         return false;
1412     }
1413 
1414     if (m_currentState == PlaylistState::VideoOnly && !m_canBeVideo) {
1415         qDebug() << "ERROR: clip is in video state but doesn't have video";
1416         return false;
1417     }
1418     if (m_currentState == PlaylistState::AudioOnly && !m_canBeAudio) {
1419         qDebug() << "ERROR: clip is in video state but doesn't have video";
1420         return false;
1421     }
1422     // TODO: check speed
1423 
1424     return true;
1425 }
1426 
1427 int ClipModel::getSubPlaylistIndex() const
1428 {
1429     return m_subPlaylistIndex;
1430 }
1431 
1432 void ClipModel::setSubPlaylistIndex(int index, int trackId)
1433 {
1434     if (m_subPlaylistIndex == index) {
1435         return;
1436     }
1437     m_subPlaylistIndex = index;
1438     if (trackId > -1) {
1439         refreshProducerFromBin(trackId);
1440     }
1441 }
1442 
1443 void ClipModel::setOffset(int offset)
1444 {
1445     m_positionOffset = offset;
1446     if (auto ptr = m_parent.lock()) {
1447         QModelIndex ix = ptr->makeClipIndexFromID(m_id);
1448         Q_EMIT ptr->dataChanged(ix, ix, {TimelineModel::PositionOffsetRole});
1449     }
1450 }
1451 
1452 void ClipModel::setGrab(bool grab)
1453 {
1454     QWriteLocker locker(&m_lock);
1455     if (grab == m_grabbed) {
1456         return;
1457     }
1458     m_grabbed = grab;
1459     if (auto ptr = m_parent.lock()) {
1460         QModelIndex ix = ptr->makeClipIndexFromID(m_id);
1461         Q_EMIT ptr->dataChanged(ix, ix, {TimelineModel::GrabbedRole});
1462     }
1463 }
1464 
1465 void ClipModel::setSelected(bool sel)
1466 {
1467     QWriteLocker locker(&m_lock);
1468     if (sel == selected) {
1469         return;
1470     }
1471     selected = sel;
1472     if (auto ptr = m_parent.lock()) {
1473         if (m_currentTrackId != -1) {
1474             QModelIndex ix = ptr->makeClipIndexFromID(m_id);
1475             Q_EMIT ptr->dataChanged(ix, ix, {TimelineModel::SelectedRole});
1476         }
1477     }
1478 }
1479 
1480 void ClipModel::clearOffset()
1481 {
1482     if (m_positionOffset != 0) {
1483         setOffset(0);
1484     }
1485 }
1486 
1487 int ClipModel::getOffset() const
1488 {
1489     return m_positionOffset;
1490 }
1491 
1492 int ClipModel::getMaxDuration() const
1493 {
1494     READ_LOCK();
1495     if (m_endlessResize) {
1496         return -1;
1497     }
1498     return m_producer->get_length();
1499 }
1500 
1501 const QString ClipModel::clipName() const
1502 {
1503     return pCore->projectItemModel()->getClipByBinID(m_binClipId)->clipName();
1504 }
1505 
1506 const QString ClipModel::clipTag() const
1507 {
1508     if (KdenliveSettings::tagsintimeline()) {
1509         return pCore->projectItemModel()->getClipByBinID(m_binClipId)->tags();
1510     }
1511     return QString();
1512 }
1513 
1514 FileStatus::ClipStatus ClipModel::clipStatus() const
1515 {
1516     std::shared_ptr<ProjectClip> binClip = pCore->projectItemModel()->getClipByBinID(m_binClipId);
1517     return binClip->clipStatus();
1518 }
1519 
1520 QString ClipModel::clipHash() const
1521 {
1522     QDomDocument document;
1523     QDomElement container = document.createElement(QStringLiteral("clip"));
1524     container.setAttribute(QStringLiteral("service"), m_producer->parent().get("mlt_service"));
1525     container.setAttribute(QStringLiteral("in"), getIn());
1526     container.setAttribute(QStringLiteral("out"), getOut());
1527     container.setAttribute(QStringLiteral("position"), getPosition());
1528     container.setAttribute(QStringLiteral("state"), m_currentState);
1529     container.setAttribute(QStringLiteral("playlist"), m_subPlaylistIndex);
1530     container.setAttribute(QStringLiteral("speed"), QString::number(m_speed, 'f'));
1531     container.setAttribute(QStringLiteral("audioStream"), getIntProperty(QStringLiteral("audio_index")));
1532     std::vector<int> snaps;
1533     allSnaps(snaps);
1534     QString snapData;
1535     for (auto &s : snaps) {
1536         snapData.append(QString::number(s));
1537     }
1538     container.setAttribute(QStringLiteral("markers"), snapData);
1539     document.appendChild(container);
1540     container.appendChild(m_effectStack->toXml(document));
1541     return document.toString();
1542 }
1543 
1544 const QString ClipModel::clipThumbPath()
1545 {
1546     std::shared_ptr<ProjectClip> binClip = pCore->projectItemModel()->getClipByBinID(m_binClipId);
1547     if (binClip) {
1548         return binClip->baseThumbPath();
1549     }
1550     return QString();
1551 }
1552 
1553 void ClipModel::switchBinReference(const QString newId, const QUuid &uuid)
1554 {
1555     deregisterClipToBin(uuid);
1556     m_binClipId = newId;
1557     refreshProducerFromBin(-1);
1558     registerClipToBin(getProducer(), false);
1559     if (auto ptr = m_parent.lock()) {
1560         ptr->replugClip(m_id);
1561         QVector<int> roles{TimelineModel::ClipThumbRole};
1562         QModelIndex ix = ptr->makeClipIndexFromID(m_id);
1563         ptr->notifyChange(ix, ix, roles);
1564         // invalidate timeline preview
1565         if (!ptr->getTrackById_const(m_currentTrackId)->isAudioTrack()) {
1566             Q_EMIT ptr->invalidateZone(m_position, m_position + getPlaytime());
1567         }
1568     }
1569 }