clipmodel.cpp 32.4 KB
Newer Older
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21
/***************************************************************************
 *   Copyright (C) 2017 by Nicolas Carion                                  *
 *   This file is part of Kdenlive. See www.kdenlive.org.                  *
 *                                                                         *
 *   This program is free software; you can redistribute it and/or modify  *
 *   it under the terms of the GNU General Public License as published by  *
 *   the Free Software Foundation; either version 2 of the License, or     *
 *   (at your option) version 3 or any later version accepted by the       *
 *   membership of KDE e.V. (or its successor approved  by the membership  *
 *   of KDE e.V.), which shall act as a proxy defined in Section 14 of     *
 *   version 3 of the license.                                             *
 *                                                                         *
 *   This program is distributed in the hope that it will be useful,       *
 *   but WITHOUT ANY WARRANTY; without even the implied warranty of        *
 *   MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the         *
 *   GNU General Public License for more details.                          *
 *                                                                         *
 *   You should have received a copy of the GNU General Public License     *
 *   along with this program.  If not, see <http://www.gnu.org/licenses/>. *
 ***************************************************************************/
#include "clipmodel.hpp"
Nicolas Carion's avatar
Nicolas Carion committed
22
#include "bin/projectclip.h"
Nicolas Carion's avatar
Nicolas Carion committed
23
#include "bin/projectitemmodel.h"
24
#include "clipsnapmodel.hpp"
Nicolas Carion's avatar
Nicolas Carion committed
25 26
#include "core.h"
#include "effects/effectstack/model/effectstackmodel.hpp"
Nicolas Carion's avatar
Nicolas Carion committed
27
#include "logger.hpp"
Nicolas Carion's avatar
linting  
Nicolas Carion committed
28
#include "macros.hpp"
29
#include "timelinemodel.hpp"
30
#include "trackmodel.hpp"
31
#include <QDebug>
32
#include <effects/effectsrepository.hpp>
Nicolas Carion's avatar
Nicolas Carion committed
33
#include <mlt++/MltProducer.h>
Nicolas Carion's avatar
linting  
Nicolas Carion committed
34
#include <utility>
35

Nicolas Carion's avatar
Nicolas Carion committed
36
ClipModel::ClipModel(const std::shared_ptr<TimelineModel> &parent, std::shared_ptr<Mlt::Producer> prod, const QString &binClipId, int id,
37
                     PlaylistState::ClipState state, double speed)
38
    : MoveableItem<Mlt::Producer>(parent, id)
Nicolas Carion's avatar
linting  
Nicolas Carion committed
39
    , m_producer(std::move(prod))
40
    , m_effectStack(EffectStackModel::construct(m_producer, {ObjectType::TimelineClip, m_id}, parent->m_undoStack))
41
    , m_clipMarkerModel(new ClipSnapModel())
42
    , m_binClipId(binClipId)
43
    , forceThumbReload(false)
44 45
    , m_currentState(state)
    , m_speed(speed)
46
    , m_fakeTrack(-1)
47
    , m_positionOffset(0)
48
{
49
    m_producer->set("kdenlive:id", binClipId.toUtf8().constData());
50
    m_producer->set("_kdenlive_cid", m_id);
51
    std::shared_ptr<ProjectClip> binClip = pCore->projectItemModel()->getClipByBinID(m_binClipId);
52 53
    m_canBeVideo = binClip->hasVideo();
    m_canBeAudio = binClip->hasAudio();
54
    m_clipType = binClip->clipType();
55
    if (binClip) {
Jean-Baptiste Mardelle's avatar
Jean-Baptiste Mardelle committed
56
        m_endlessResize = !binClip->hasLimitedDuration();
57 58 59
    } else {
        m_endlessResize = false;
    }
60 61
    QObject::connect(m_effectStack.get(), &EffectStackModel::dataChanged, [&](const QModelIndex &, const QModelIndex &, QVector<int> roles) {
        qDebug() << "// GOT CLIP STACK DATA CHANGE: " << roles;
62 63 64
        if (m_currentTrackId != -1) {
            if (auto ptr = m_parent.lock()) {
                QModelIndex ix = ptr->makeClipIndexFromID(m_id);
65
                qDebug() << "// GOT CLIP STACK DATA CHANGE DONE: " << ix << " = " << roles;
66
                ptr->dataChanged(ix, ix, roles);
67
            }
68 69
        }
    });
70 71
}

72
int ClipModel::construct(const std::shared_ptr<TimelineModel> &parent, const QString &binClipId, int id, PlaylistState::ClipState state, int audioStream, double speed, bool warp_pitch)
73
{
74
    id = (id == -1 ? TimelineModel::getNextId() : id);
75
    std::shared_ptr<ProjectClip> binClip = pCore->projectItemModel()->getClipByBinID(binClipId);
76 77 78 79 80 81

    // We refine the state according to what the clip can actually produce
    std::pair<bool, bool> videoAudio = stateToBool(state);
    videoAudio.first = videoAudio.first && binClip->hasVideo();
    videoAudio.second = videoAudio.second && binClip->hasAudio();
    state = stateFromBool(videoAudio);
82 83
    qDebug()<<"// GET TIMELINE PROD FOR STREAM: "<<audioStream;
    std::shared_ptr<Mlt::Producer> cutProducer = binClip->getTimelineProducer(-1, id, state, audioStream, speed);
84
    std::shared_ptr<ClipModel> clip(new ClipModel(parent, cutProducer, binClipId, id, state, speed));
85 86 87
    if (!qFuzzyCompare(speed, 1.)) {
        cutProducer->parent().set("warp_pitch", warp_pitch ? 1 : 0);
    }
88
    qDebug()<<"==== BUILT CLIP STREAM: "<<clip->audioStream();
Nicolas Carion's avatar
Nicolas Carion committed
89
    TRACE_CONSTR(clip.get(), parent, binClipId, id, state, speed);
90
    clip->setClipState_lambda(state)();
91
    parent->registerClip(clip);
92
    clip->m_clipMarkerModel->setReferenceModel(binClip->getMarkerModel(), speed);
93
    return id;
94 95
}

96
void ClipModel::allSnaps(std::vector<int> &snaps, int offset)
97
{
98
    m_clipMarkerModel->allSnaps(snaps, offset);
99 100
}

Nicolas Carion's avatar
Nicolas Carion committed
101
int ClipModel::construct(const std::shared_ptr<TimelineModel> &parent, const QString &binClipId, const std::shared_ptr<Mlt::Producer> &producer,
102
                         PlaylistState::ClipState state, int tid, QString originalDecimalPoint)
103
{
104 105 106 107 108

    // we hand the producer to the bin clip, and in return we get a cut to a good master producer
    // 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
    // clipModel (due to a mlt limitation, see ProjectClip doc)

109
    int id = TimelineModel::getNextId();
110 111 112 113 114 115 116 117
    std::shared_ptr<ProjectClip> binClip = pCore->projectItemModel()->getClipByBinID(binClipId);

    // We refine the state according to what the clip can actually produce
    std::pair<bool, bool> videoAudio = stateToBool(state);
    videoAudio.first = videoAudio.first && binClip->hasVideo();
    videoAudio.second = videoAudio.second && binClip->hasAudio();
    state = stateFromBool(videoAudio);

118
    double speed = 1.0;
119
    bool warp_pitch = false;
120 121
    if (QString::fromUtf8(producer->parent().get("mlt_service")) == QLatin1String("timewarp")) {
        speed = producer->parent().get_double("warp_speed");
122
        warp_pitch = producer->parent().get_int("warp_pitch");
123
    }
124
    auto result = binClip->giveMasterAndGetTimelineProducer(id, producer, state, tid);
125
    std::shared_ptr<ClipModel> clip(new ClipModel(parent, result.first, binClipId, id, state, speed));
126 127 128
    if (warp_pitch) {
        result.first->parent().set("warp_pitch", 1);
    }
129
    clip->setClipState_lambda(state)();
130
    parent->registerClip(clip);
131
    clip->m_effectStack->importEffects(producer, state, result.second, originalDecimalPoint);
132
    clip->m_clipMarkerModel->setReferenceModel(binClip->getMarkerModel(), speed);
133 134 135
    return id;
}

136
void ClipModel::registerClipToBin(std::shared_ptr<Mlt::Producer> service, bool registerProducer)
137
{
138
    std::shared_ptr<ProjectClip> binClip = pCore->projectItemModel()->getClipByBinID(m_binClipId);
139 140 141
    if (!binClip) {
        qDebug() << "Error : Bin clip for id: " << m_binClipId << " NOT AVAILABLE!!!";
    }
Nicolas Carion's avatar
Nicolas Carion committed
142
    qDebug() << "REGISTRATION " << m_id << "ptr count" << m_parent.use_count();
Nicolas Carion's avatar
Nicolas Carion committed
143
    binClip->registerService(m_parent, m_id, std::move(service), registerProducer);
144 145 146
}

void ClipModel::deregisterClipToBin()
147
{
148
    std::shared_ptr<ProjectClip> binClip = pCore->projectItemModel()->getClipByBinID(m_binClipId);
149 150
    binClip->deregisterTimelineClip(m_id);
}
151

Nicolas Carion's avatar
Nicolas Carion committed
152
ClipModel::~ClipModel() = default;
153

154
bool ClipModel::requestResize(int size, bool right, Fun &undo, Fun &redo, bool logUndo)
155
{
Nicolas Carion's avatar
Nicolas Carion committed
156
    QWriteLocker locker(&m_lock);
157 158
    // qDebug() << "RESIZE CLIP" << m_id << "target size=" << size << "right=" << right << "endless=" << m_endlessResize << "length" <<
    // m_producer->get_length();
159
    if (!m_endlessResize && (size <= 0 || size > m_producer->get_length())) {
160 161
        return false;
    }
162
    int delta = getPlaytime() - size;
163
    if (delta == 0) {
164
        return true;
165
    }
166 167
    int in = m_producer->get_in();
    int out = m_producer->get_out();
168 169
    int oldIn = m_position;
    int oldOut = m_position + out - in;
170
    int old_in = in, old_out = out;
Nicolas Carion's avatar
Nicolas Carion committed
171
    // check if there is enough space on the chosen side
172 173 174 175 176 177 178
    if (!m_endlessResize) {
        if (!right && in + delta < 0) {
            return false;
        }
        if (right && (out - delta >= m_producer->get_length())) {
            return false;
        }
179 180 181 182 183 184
    }
    if (right) {
        out -= delta;
    } else {
        in += delta;
    }
Nicolas Carion's avatar
Nicolas Carion committed
185
    // qDebug() << "Resize facts delta =" << delta << "old in" << old_in << "old_out" << old_out << "in" << in << "out" << out;
Nicolas Carion's avatar
Nicolas Carion committed
186 187
    std::function<bool(void)> track_operation = []() { return true; };
    std::function<bool(void)> track_reverse = []() { return true; };
188 189
    int outPoint = out;
    int inPoint = in;
190
    int offset = 0;
191
    int trackDuration = 0;
192
    if (m_endlessResize) {
193
        offset = inPoint;
194 195 196
        outPoint = out - in;
        inPoint = 0;
    }
197 198
    if (m_currentTrackId != -1) {
        if (auto ptr = m_parent.lock()) {
199 200 201
            if (ptr->getTrackById(m_currentTrackId)->isLocked()) {
                return false;
            }
202 203 204
            if (right && ptr->getTrackById_const(m_currentTrackId)->isLastClip(getPosition())) {
                trackDuration = ptr->getTrackById_const(m_currentTrackId)->trackDuration();
            }
205
            track_operation = ptr->getTrackById(m_currentTrackId)->requestClipResize_lambda(m_id, inPoint, outPoint, right);
206 207 208 209
        } else {
            qDebug() << "Error : Moving clip failed because parent timeline is not available anymore";
            Q_ASSERT(false);
        }
210 211 212 213 214
    } else {
        // Ensure producer is long enough
        if (m_endlessResize && outPoint > m_producer->parent().get_length()) {
            m_producer->set("length", outPoint + 1);
        }
215
    }
216 217 218 219 220 221 222
    QVector<int> roles{TimelineModel::DurationRole};
    if (!right) {
        roles.push_back(TimelineModel::StartRole);
        roles.push_back(TimelineModel::InPointRole);
    } else {
        roles.push_back(TimelineModel::OutPointRole);
    }
223

224
    Fun operation = [this, inPoint, outPoint, roles, oldIn, oldOut, right, logUndo, track_operation]() {
225
        if (track_operation()) {
226
            setInOut(inPoint, outPoint);
227 228 229
            if (m_currentTrackId > -1) {
                if (auto ptr = m_parent.lock()) {
                    QModelIndex ix = ptr->makeClipIndexFromID(m_id);
Jean-Baptiste Mardelle's avatar
Jean-Baptiste Mardelle committed
230
                    ptr->notifyChange(ix, ix, roles);
231 232 233 234 235 236 237 238 239 240 241 242 243 244 245 246 247
                    // invalidate timeline preview
                    if (logUndo) {                        
                        if (right) {
                            int newOut = m_position + getOut() - getIn();
                            if (oldOut < newOut) {
                                ptr->invalidateZone(oldOut, newOut);
                            } else {
                                ptr->invalidateZone(newOut, oldOut);
                            }
                        } else {
                            if (oldIn < m_position) {
                                ptr->invalidateZone(oldIn, m_position);
                            } else {
                                ptr->invalidateZone(m_position, oldIn);
                            }
                        }
                    }
248 249
                }
            }
250 251 252
            return true;
        }
        return false;
253
    };
254
    if (operation()) {
255 256 257 258 259
        Fun reverse = []() { return true; };
        if (logUndo) {
            // 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
            if (m_currentTrackId != -1) {
                if (auto ptr = m_parent.lock()) {
260 261 262 263 264 265 266 267
                    if (trackDuration > 0) {
                        // Operation changed parent track duration, update effect stack
                        int newDuration = ptr->getTrackById_const(m_currentTrackId)->trackDuration();
                        if (logUndo || trackDuration != newDuration) {
                            // A clip move changed the track duration, update track effects
                            ptr->getTrackById(m_currentTrackId)->m_effectStack->adjustStackLength(true, 0, trackDuration, 0, newDuration, 0, undo, redo, logUndo);
                        }
                    }
268 269
                    track_reverse = ptr->getTrackById(m_currentTrackId)->requestClipResize_lambda(m_id, old_in, old_out, right);
                }
270
            }
271
            reverse = [this, old_in, old_out, track_reverse, logUndo, oldIn, oldOut, right, roles]() {
272 273 274 275 276
                if (track_reverse()) {
                    setInOut(old_in, old_out);
                    if (m_currentTrackId > -1) {
                        if (auto ptr = m_parent.lock()) {
                            QModelIndex ix = ptr->makeClipIndexFromID(m_id);
Jean-Baptiste Mardelle's avatar
Jean-Baptiste Mardelle committed
277
                            ptr->notifyChange(ix, ix, roles);
278 279 280 281 282 283 284 285 286 287 288 289 290 291 292 293
                            if (logUndo) {                        
                                if (right) {
                                    int newOut = m_position + getOut() - getIn();
                                    if (oldOut < newOut) {
                                        ptr->invalidateZone(oldOut, newOut);
                                    } else {
                                        ptr->invalidateZone(newOut, oldOut);
                                    }
                                } else {
                                    if (oldIn < m_position) {
                                        ptr->invalidateZone(oldIn, m_position);
                                    } else {
                                        ptr->invalidateZone(m_position, oldIn);
                                    }
                                }
                            }
294
                        }
295
                    }
296
                    return true;
297
                }
298 299 300
                return false;
            };
            qDebug() << "----------\n-----------\n// ADJUSTING EFFECT LENGTH, LOGUNDO " << logUndo << ", " << old_in << "/" << inPoint << ", "
301
                 << m_producer->get_playtime();
302

303
            adjustEffectLength(right, old_in, inPoint, old_out - old_in, m_producer->get_playtime(), offset, reverse, operation, logUndo);
Nicolas Carion's avatar
Nicolas Carion committed
304
        }
305
        UPDATE_UNDO_REDO(operation, reverse, undo, redo);
306
        return true;
307 308
    }
    return false;
309 310
}

311 312
const QString ClipModel::getProperty(const QString &name) const
{
Nicolas Carion's avatar
Nicolas Carion committed
313
    READ_LOCK();
314 315 316 317 318 319
    if (service()->parent().is_valid()) {
        return QString::fromUtf8(service()->parent().get(name.toUtf8().constData()));
    }
    return QString::fromUtf8(service()->get(name.toUtf8().constData()));
}

320 321
int ClipModel::getIntProperty(const QString &name) const
{
Nicolas Carion's avatar
Nicolas Carion committed
322
    READ_LOCK();
323 324 325 326 327 328
    if (service()->parent().is_valid()) {
        return service()->parent().get_int(name.toUtf8().constData());
    }
    return service()->get_int(name.toUtf8().constData());
}

329 330 331 332 333 334
QSize ClipModel::getFrameSize() const
{
    READ_LOCK();
    if (service()->parent().is_valid()) {
        return QSize(service()->parent().get_int("meta.media.width"), service()->parent().get_int("meta.media.height"));
    }
Nicolas Carion's avatar
Nicolas Carion committed
335
    return {service()->get_int("meta.media.width"), service()->get_int("meta.media.height")};
336 337
}

338 339 340 341 342 343 344 345 346
double ClipModel::getDoubleProperty(const QString &name) const
{
    READ_LOCK();
    if (service()->parent().is_valid()) {
        return service()->parent().get_double(name.toUtf8().constData());
    }
    return service()->get_double(name.toUtf8().constData());
}

Nicolas Carion's avatar
Nicolas Carion committed
347
Mlt::Producer *ClipModel::service() const
348
{
Nicolas Carion's avatar
Nicolas Carion committed
349
    READ_LOCK();
350
    return m_producer.get();
351 352
}

353 354 355 356 357 358
std::shared_ptr<Mlt::Producer> ClipModel::getProducer()
{
    READ_LOCK();
    return m_producer;
}

359
int ClipModel::getPlaytime() const
360
{
Nicolas Carion's avatar
Nicolas Carion committed
361
    READ_LOCK();
362
    return m_producer->get_playtime();
363
}
364 365 366

void ClipModel::setTimelineEffectsEnabled(bool enabled)
{
Nicolas Carion's avatar
Nicolas Carion committed
367
    QWriteLocker locker(&m_lock);
368
    m_effectStack->setEffectStackEnabled(enabled);
369
}
370

371 372
bool ClipModel::addEffect(const QString &effectId)
{
Nicolas Carion's avatar
Nicolas Carion committed
373
    QWriteLocker locker(&m_lock);
374 375
    AssetListType::AssetType type = EffectsRepository::get()->getType(effectId);
    if (type == AssetListType::AssetType::Audio || type == AssetListType::AssetType::CustomAudio) {
376 377 378 379 380 381
        if (m_currentState == PlaylistState::VideoOnly) {
            return false;
        }
    } else if (m_currentState == PlaylistState::AudioOnly) {
        return false;
    }
382
    m_effectStack->appendEffect(effectId, true);
383 384 385
    return true;
}

Nicolas Carion's avatar
Nicolas Carion committed
386
bool ClipModel::copyEffect(const std::shared_ptr<EffectStackModel> &stackModel, int rowId)
387
{
Nicolas Carion's avatar
Nicolas Carion committed
388
    QWriteLocker locker(&m_lock);
389 390
    QDomDocument doc;
    m_effectStack->copyXmlEffect(stackModel->rowToXml(rowId, doc));
391 392 393
    return true;
}

394 395
bool ClipModel::importEffects(std::shared_ptr<EffectStackModel> stackModel)
{
Nicolas Carion's avatar
Nicolas Carion committed
396
    QWriteLocker locker(&m_lock);
Nicolas Carion's avatar
Nicolas Carion committed
397
    m_effectStack->importEffects(std::move(stackModel), m_currentState);
398 399 400
    return true;
}

401 402 403
bool ClipModel::importEffects(std::weak_ptr<Mlt::Service> service)
{
    QWriteLocker locker(&m_lock);
Nicolas Carion's avatar
Nicolas Carion committed
404
    m_effectStack->importEffects(std::move(service), m_currentState);
405 406 407
    return true;
}

408
bool ClipModel::removeFade(bool fromStart)
409
{
Nicolas Carion's avatar
Nicolas Carion committed
410
    QWriteLocker locker(&m_lock);
411
    m_effectStack->removeFade(fromStart);
412 413 414
    return true;
}

415
bool ClipModel::adjustEffectLength(bool adjustFromEnd, int oldIn, int newIn, int oldDuration, int duration, int offset, Fun &undo, Fun &redo, bool logUndo)
416
{
Nicolas Carion's avatar
Nicolas Carion committed
417
    QWriteLocker locker(&m_lock);
418
    return m_effectStack->adjustStackLength(adjustFromEnd, oldIn, oldDuration, newIn, duration, offset, undo, redo, logUndo);
419 420
}

421
bool ClipModel::adjustEffectLength(const QString &effectName, int duration, int originalDuration, Fun &undo, Fun &redo)
422
{
Nicolas Carion's avatar
Nicolas Carion committed
423
    QWriteLocker locker(&m_lock);
424
    qDebug() << ".... ADJUSTING FADE LENGTH: " << duration << " / " << effectName;
425
    Fun operation = [this, duration, effectName, originalDuration]() {
426
        return m_effectStack->adjustFadeLength(duration, effectName == QLatin1String("fadein") || effectName == QLatin1String("fade_to_black"), audioEnabled(),
427
                                               !isAudioOnly(), originalDuration > 0);
428 429 430
    };
    if (operation() && originalDuration > 0) {
        Fun reverse = [this, originalDuration, effectName]() {
431
            return m_effectStack->adjustFadeLength(originalDuration, effectName == QLatin1String("fadein") || effectName == QLatin1String("fade_to_black"),
432
                                                   audioEnabled(), !isAudioOnly(), true);
433
        };
Nicolas Carion's avatar
Nicolas Carion committed
434
        UPDATE_UNDO_REDO(operation, reverse, undo, redo);
435
    }
436 437 438
    return true;
}

439
bool ClipModel::audioEnabled() const
440
{
Nicolas Carion's avatar
Nicolas Carion committed
441
    READ_LOCK();
442
    return stateToBool(m_currentState).second;
443
}
444 445 446

bool ClipModel::isAudioOnly() const
{
Nicolas Carion's avatar
Nicolas Carion committed
447
    READ_LOCK();
448
    return m_currentState == PlaylistState::AudioOnly;
449 450
}

451
void ClipModel::refreshProducerFromBin(int trackId, PlaylistState::ClipState state, int stream, double speed, bool hasPitch)
452
{
453 454
    // 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
    // first, refresh, and then replant.
Nicolas Carion's avatar
Nicolas Carion committed
455
    QWriteLocker locker(&m_lock);
456 457
    int in = getIn();
    int out = getOut();
458
    if (!qFuzzyCompare(speed, m_speed) && !qFuzzyIsNull(speed)) {
459
        in = in * std::abs(m_speed / speed);
460
        out = in + getPlaytime() - 1;
461
        // prevent going out of the clip's range
462
        out = std::min(out, int(double(m_producer->get_length()) * std::abs(m_speed / speed)) - 1);
463 464 465
        m_speed = speed;
        qDebug() << "changing speed" << in << out << m_speed;
    }
466
    std::shared_ptr<ProjectClip> binClip = pCore->projectItemModel()->getClipByBinID(m_binClipId);
467
    std::shared_ptr<Mlt::Producer> binProducer = binClip->getTimelineProducer(trackId, m_id, state, stream, m_speed);
468 469
    m_producer = std::move(binProducer);
    m_producer->set_in_and_out(in, out);
470 471 472 473 474 475
    if (hasPitch) {
        // Check if pitch shift is enabled
        m_producer->parent().set("warp_pitch", 1);
    } else if (!qFuzzyCompare(m_speed, 1.)) {
        m_producer->parent().set("warp_pitch", 0);
    }
Jean-Baptiste Mardelle's avatar
Jean-Baptiste Mardelle committed
476 477
    // replant effect stack in updated service
    m_effectStack->resetService(m_producer);
478
    m_producer->set("kdenlive:id", binClip->clipId().toUtf8().constData());
479
    m_producer->set("_kdenlive_cid", m_id);
Jean-Baptiste Mardelle's avatar
Jean-Baptiste Mardelle committed
480
    m_endlessResize = !binClip->hasLimitedDuration();
481 482
}

483
void ClipModel::refreshProducerFromBin(int trackId)
484
{
485 486 487
    if (trackId == -1) {
        trackId = m_currentTrackId;
    }
488 489 490 491
    bool hasPitch = false;
    if (!qFuzzyCompare(getSpeed(), 1.)) {
        hasPitch = m_producer->parent().get_int("warp_pitch") == 1;
    }
492 493
    int stream = m_producer->parent().get_int("audio_index");
    refreshProducerFromBin(trackId, m_currentState, stream, 0, hasPitch);
494
}
495

496
bool ClipModel::useTimewarpProducer(double speed, bool pitchCompensate, bool changeDuration, Fun &undo, Fun &redo)
497
{
498
    if (m_endlessResize) {
Nicolas Carion's avatar
Nicolas Carion committed
499 500 501 502 503
        // no timewarp for endless producers
        return false;
    }
    std::function<bool(void)> local_undo = []() { return true; };
    std::function<bool(void)> local_redo = []() { return true; };
504
    double previousSpeed = getSpeed();
505
    int oldDuration = getPlaytime();
506
    int newDuration = qRound(oldDuration * std::fabs(m_speed / speed));
507 508
    int oldOut = getOut();
    int oldIn = getIn();
509 510 511 512 513 514 515 516
    bool revertSpeed = false;
    if (speed < 0) {
        if (previousSpeed > 0) {
            revertSpeed = true;
        }
    } else if (previousSpeed < 0) {
        revertSpeed = true;
    }
517
    bool hasPitch = getIntProperty(QStringLiteral("warp_pitch"));
518 519 520
    int audioStream = getIntProperty(QStringLiteral("audio_index"));
    auto operation = useTimewarpProducer_lambda(speed, audioStream, pitchCompensate);
    auto reverse = useTimewarpProducer_lambda(previousSpeed, audioStream, hasPitch);
521
    if (revertSpeed || (changeDuration && oldOut >= newDuration)) {
522 523 524 525 526 527 528 529 530
        // in that case, we are going to shrink the clip when changing the producer. We must undo that when reloading the old producer
        reverse = [reverse, oldIn, oldOut, this]() {
            bool res = reverse();
            if (res) {
                setInOut(oldIn, oldOut);
            }
            return res;
        };
    }
531 532 533
    if (revertSpeed) {
        int in = getIn();
        int out = getOut();
534
        in = qMax(0, qRound((m_producer->get_length() - 1 - out)* std::fabs(m_speed/speed) + 0.5));
535
        out = in + newDuration;
536 537 538 539
        operation = [operation, in, out, this]() {
            bool res = operation();
            if (res) {
                setInOut(in, out);
540
            } else {
541 542 543 544 545
            }
            return res;
        };

    }
546 547
    if (operation()) {
        UPDATE_UNDO_REDO(operation, reverse, local_undo, local_redo);
548
        // When calculating duration, result can be a few frames longer than possible duration so adjust
549
        if (changeDuration) {
550 551 552 553 554 555 556 557
            int requestedDuration = qMin(newDuration, getMaxDuration() - getIn());
            if (requestedDuration != getPlaytime()) {
                bool res = requestResize(requestedDuration, true, local_undo, local_redo, true);
                if (!res) {
                    qDebug()<<"==== CLIP WARP UPDATE DURATION FAILED!!!!";
                    local_undo();
                    return false;
                }
558
            }
Jean-Baptiste Mardelle's avatar
Jean-Baptiste Mardelle committed
559
        }
560
        adjustEffectLength(false, oldIn, getIn(), oldOut - oldIn, m_producer->get_playtime(), 0, local_undo, local_redo, true);
561 562
        UPDATE_UNDO_REDO(local_redo, local_undo, undo, redo);
        return true;
Jean-Baptiste Mardelle's avatar
Jean-Baptiste Mardelle committed
563
    }
564
    qDebug() << "tw: operation fail";
565
    return false;
566 567
}

568
Fun ClipModel::useTimewarpProducer_lambda(double speed, int stream, bool pitchCompensate)
569 570
{
    QWriteLocker locker(&m_lock);
571
    return [speed, stream, pitchCompensate, this]() {
572
        qDebug() << "timeWarp producer" << speed;
573
        refreshProducerFromBin(m_currentTrackId, m_currentState, stream, speed, pitchCompensate);
574 575
        return true;
    };
576 577
}

578
const QString &ClipModel::binId() const
579
{
580
    return m_binClipId;
581
}
582 583 584

std::shared_ptr<MarkerListModel> ClipModel::getMarkerModel() const
{
Nicolas Carion's avatar
Nicolas Carion committed
585
    READ_LOCK();
586
    return pCore->projectItemModel()->getClipByBinID(m_binClipId)->getMarkerModel();
587
}
588

589 590 591 592 593 594
int ClipModel::audioChannels() const
{
    READ_LOCK();
    return pCore->projectItemModel()->getClipByBinID(m_binClipId)->audioChannels();
}

595 596 597 598 599 600 601
int ClipModel::audioStream() const
{
    READ_LOCK();
    if (pCore->projectItemModel()->getClipByBinID(m_binClipId)->audioStreamsCount() > 1) {
        return m_producer->parent().get_int("audio_index");
    }
    return -m_producer->parent().get_int("audio_index");
602 603 604 605 606 607 608 609
}

int ClipModel::audioStreamIndex() const
{
    READ_LOCK();
    QVariantList list;
    QList <int> streams = pCore->projectItemModel()->getClipByBinID(m_binClipId)->audioStreams().keys();
    return streams.indexOf(m_producer->parent().get_int("audio_index")) + 1;
610 611
}

612 613
int ClipModel::fadeIn() const
{
614
    return m_effectStack->getFadePosition(true);
615 616 617 618
}

int ClipModel::fadeOut() const
{
Jean-Baptiste Mardelle's avatar
Jean-Baptiste Mardelle committed
619
    return m_effectStack->getFadePosition(false);
620 621
}

622 623
double ClipModel::getSpeed() const
{
Nicolas Carion's avatar
Nicolas Carion committed
624
    return m_speed;
625
}
626 627 628 629 630 631 632 633 634

KeyframeModel *ClipModel::getKeyframeModel()
{
    return m_effectStack->getEffectKeyframeModel();
}

bool ClipModel::showKeyframes() const
{
    READ_LOCK();
635
    return !service()->get_int("kdenlive:hide_keyframes");
636 637 638 639 640
}

void ClipModel::setShowKeyframes(bool show)
{
    QWriteLocker locker(&m_lock);
641
    service()->set("kdenlive:hide_keyframes", (int)!show);
642
}
643

644 645 646 647 648 649 650 651 652 653 654 655
void ClipModel::setPosition(int pos)
{
    MoveableItem::setPosition(pos);
    m_clipMarkerModel->updateSnapModelPos(pos);
}

void ClipModel::setInOut(int in, int out)
{
    MoveableItem::setInOut(in, out);
    m_clipMarkerModel->updateSnapModelInOut(std::pair<int, int>(in, out));
}

656 657 658 659 660
void ClipModel::setCurrentTrackId(int tid, bool finalMove)
{
    if (tid == m_currentTrackId) {
        return;
    }
661
    bool registerSnap = m_currentTrackId == -1 && tid > -1;
662

663 664 665 666
    if (m_currentTrackId > -1 && tid == -1) {
        // Removing clip
        m_clipMarkerModel->deregisterSnapModel();
    }
Jean-Baptiste Mardelle's avatar
Jean-Baptiste Mardelle committed
667
    MoveableItem::setCurrentTrackId(tid, finalMove);
668 669
    if (registerSnap) {
        if (auto ptr = m_parent.lock()) {
670
            m_clipMarkerModel->registerSnapModel(ptr->m_snaps, getPosition(), getIn(), getOut(), m_speed);
671 672 673
        }
    }

674
    if (finalMove && tid != -1 && m_lastTrackId != m_currentTrackId) {
675
        refreshProducerFromBin(m_currentTrackId);
676
        m_lastTrackId = m_currentTrackId;
677 678 679
    }
}

680
Fun ClipModel::setClipState_lambda(PlaylistState::ClipState state)
681
{
Nicolas Carion's avatar
Nicolas Carion committed
682
    QWriteLocker locker(&m_lock);
683 684 685
    return [this, state]() {
        if (auto ptr = m_parent.lock()) {
            m_currentState = state;
686 687
            // Enforce producer reload
            m_lastTrackId = -1;
688
            if (m_currentTrackId != -1 && ptr->isClip(m_id)) { // if this is false, the clip is being created. Don't update model in that case
689
                refreshProducerFromBin(m_currentTrackId);
690 691 692
                QModelIndex ix = ptr->makeClipIndexFromID(m_id);
                ptr->dataChanged(ix, ix, {TimelineModel::StatusRole});
            }
693 694 695 696 697 698
            return true;
        }
        return false;
    };
}

699
bool ClipModel::setClipState(PlaylistState::ClipState state, Fun &undo, Fun &redo)
700 701 702 703 704 705 706
{
    if (state == PlaylistState::VideoOnly && !canBeVideo()) {
        return false;
    }
    if (state == PlaylistState::AudioOnly && !canBeAudio()) {
        return false;
    }
707 708 709
    if (state == m_currentState) {
        return true;
    }
710 711 712 713 714 715 716 717
    auto old_state = m_currentState;
    auto operation = setClipState_lambda(state);
    if (operation()) {
        auto reverse = setClipState_lambda(old_state);
        UPDATE_UNDO_REDO(operation, reverse, undo, redo);
        return true;
    }
    return false;
718 719
}

720
PlaylistState::ClipState ClipModel::clipState() const
721
{
Nicolas Carion's avatar
Nicolas Carion committed
722
    READ_LOCK();
723
    return m_currentState;
724 725 726 727 728 729
}

ClipType::ProducerType ClipModel::clipType() const
{
    READ_LOCK();
    return m_clipType;
730
}
731

Nicolas Carion's avatar
Nicolas Carion committed
732
void ClipModel::passTimelineProperties(const std::shared_ptr<ClipModel> &other)
733
{
Nicolas Carion's avatar
Nicolas Carion committed
734
    READ_LOCK();
735 736 737 738
    Mlt::Properties source(m_producer->get_properties());
    Mlt::Properties dest(other->service()->get_properties());
    dest.pass_list(source, "kdenlive:hide_keyframes,kdenlive:activeeffect");
}
739 740 741 742 743

bool ClipModel::canBeVideo() const
{
    return m_canBeVideo;
}
744

745 746 747 748
bool ClipModel::canBeAudio() const
{
    return m_canBeAudio;
}
749 750 751 752 753 754

const QString ClipModel::effectNames() const
{
    READ_LOCK();
    return m_effectStack->effectNames();
}
755 756 757 758 759 760 761 762 763 764 765 766 767 768 769 770 771 772 773 774

int ClipModel::getFakeTrackId() const
{
    return m_fakeTrack;
}

void ClipModel::setFakeTrackId(int fid)
{
    m_fakeTrack = fid;
}

int ClipModel::getFakePosition() const
{
    return m_fakePosition;
}

void ClipModel::setFakePosition(int fid)
{
    m_fakePosition = fid;
}
775 776 777

QDomElement ClipModel::toXml(QDomDocument &document)
{
778
    QLocale locale;
779 780 781 782 783 784
    QDomElement container = document.createElement(QStringLiteral("clip"));
    container.setAttribute(QStringLiteral("binid"), m_binClipId);
    container.setAttribute(QStringLiteral("id"), m_id);
    container.setAttribute(QStringLiteral("in"), getIn());
    container.setAttribute(QStringLiteral("out"), getOut());
    container.setAttribute(QStringLiteral("position"), getPosition());
785
    container.setAttribute(QStringLiteral("state"), (int)m_currentState);
786
    if (auto ptr = m_parent.lock()) {
787
        int trackId = ptr->getTrackPosition(m_currentTrackId);
788
        container.setAttribute(QStringLiteral("track"), trackId);
789 790
        if (ptr->isAudioTrack(getCurrentTrackId())) {
            container.setAttribute(QStringLiteral("audioTrack"), 1);