1 /*
2 * Copyright (c) 2013-2021 Meltytech, LLC
3 *
4 * This program is free software: you can redistribute it and/or modify
5 * it under the terms of the GNU General Public License as published by
6 * the Free Software Foundation, either version 3 of the License, or
7 * (at your option) any later version.
8 *
9 * This program is distributed in the hope that it will be useful,
10 * but WITHOUT ANY WARRANTY; without even the implied warranty of
11 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
12 * GNU General Public License for more details.
13 *
14 * You should have received a copy of the GNU General Public License
15 * along with this program. If not, see <http://www.gnu.org/licenses/>.
16 */
17
18 #include "multitrackmodel.h"
19 #include "mltcontroller.h"
20 #include "mainwindow.h"
21 #include "settings.h"
22 #include "docks/playlistdock.h"
23 #include "util.h"
24 #include "audiolevelstask.h"
25 #include "shotcut_mlt_properties.h"
26 #include "controllers/filtercontroller.h"
27 #include "qmltypes/qmlmetadata.h"
28 #include "proxymanager.h"
29 #include "dialogs/longuitask.h"
30
31 #include <QScopedPointer>
32 #include <QApplication>
33 #include <qmath.h>
34 #include <QTimer>
35 #include <QMessageBox>
36
37 #include <Logger.h>
38
39 static const quintptr NO_PARENT_ID = quintptr(-1);
40 static const char* kShotcutDefaultTransition = "lumaMix";
41
MultitrackModel(QObject * parent)42 MultitrackModel::MultitrackModel(QObject *parent)
43 : QAbstractItemModel(parent)
44 , m_tractor(0)
45 , m_isMakingTransition(false)
46 {
47 connect(this, SIGNAL(modified()), SLOT(adjustBackgroundDuration()));
48 connect(this, SIGNAL(modified()), SLOT(adjustTrackFilters()));
49 connect(this, SIGNAL(reloadRequested()), SLOT(reload()), Qt::QueuedConnection);
50 }
51
~MultitrackModel()52 MultitrackModel::~MultitrackModel()
53 {
54 delete m_tractor;
55 m_tractor = 0;
56 }
57
rowCount(const QModelIndex & parent) const58 int MultitrackModel::rowCount(const QModelIndex &parent) const
59 {
60 if (!m_tractor)
61 return 0;
62 if (parent.isValid()) {
63 if (parent.internalId() != NO_PARENT_ID)
64 return 0;
65 int i = m_trackList.at(parent.row()).mlt_index;
66 QScopedPointer<Mlt::Producer> track(m_tractor->track(i));
67 if (track) {
68 Mlt::Playlist playlist(*track);
69 int n = playlist.count();
70 // LOG_DEBUG() << __FUNCTION__ << parent << i << n;
71 return n;
72 } else {
73 return 0;
74 }
75 }
76 return m_trackList.count();
77 }
78
columnCount(const QModelIndex & parent) const79 int MultitrackModel::columnCount(const QModelIndex &parent) const
80 {
81 Q_UNUSED(parent);
82 return 1;
83 }
84
data(const QModelIndex & index,int role) const85 QVariant MultitrackModel::data(const QModelIndex &index, int role) const
86 {
87 if (!m_tractor || !index.isValid())
88 return QVariant();
89 if (index.parent().isValid()) {
90 // Get data for a clip.
91 int i = m_trackList.at(index.internalId()).mlt_index;
92 QScopedPointer<Mlt::Producer> track(m_tractor->track(i));
93 if (track) {
94 Mlt::Playlist playlist(*track);
95 // LOG_DEBUG() << __FUNCTION__ << index.row();
96 QScopedPointer<Mlt::ClipInfo> info(playlist.clip_info(index.row()));
97 if (info)
98 switch (role) {
99 case NameRole: {
100 QString result;
101 if (info->producer && info->producer->is_valid()) {
102 result = info->producer->get(kShotcutCaptionProperty);
103 if (result.isNull()) {
104 result = Util::baseName(ProxyManager::resource(*info->producer));
105 if (!::qstrcmp(info->producer->get("mlt_service"), "timewarp")) {
106 double speed = ::fabs(info->producer->get_double("warp_speed"));
107 result = QString("%1 (%2x)").arg(result).arg(speed);
108 }
109 }
110 if (result == "<producer>") {
111 result = QString::fromUtf8(info->producer->get("mlt_service"));
112 }
113 if (info->producer->get_int(kIsProxyProperty)) {
114 result.append("\n" + tr("(PROXY)"));
115 }
116 }
117 return result;
118 }
119 case ResourceRole:
120 case Qt::DisplayRole: {
121 QString result = QString::fromUtf8(info->resource);
122 if (result == "<producer>" && info->producer
123 && info->producer->is_valid() && info->producer->get("mlt_service"))
124 result = QString::fromUtf8(info->producer->get("mlt_service"));
125 return result;
126 }
127 case ServiceRole:
128 if (info->producer && info->producer->is_valid())
129 return QString::fromUtf8(info->producer->get("mlt_service"));
130 break;
131 case IsBlankRole:
132 return playlist.is_blank(index.row());
133 case StartRole:
134 return info->start;
135 case DurationRole:
136 return info->frame_count;
137 case InPointRole:
138 return info->frame_in;
139 case OutPointRole:
140 return info->frame_out;
141 case FramerateRole:
142 return info->fps;
143 case IsAudioRole:
144 return m_trackList[index.internalId()].type == AudioTrackType;
145 case AudioLevelsRole: {
146 QVariant result;
147 if (info->producer && info->producer->is_valid()) {
148 info->producer->lock();
149 if (info->producer->get_data(kAudioLevelsProperty)) {
150 result = QVariant::fromValue(*((QVariantList*) info->producer->get_data(kAudioLevelsProperty)));
151 }
152 info->producer->unlock();
153 }
154 return result;
155 }
156 case FadeInRole: {
157 QScopedPointer<Mlt::Filter> filter(getFilter("fadeInVolume", info->producer));
158 if (!filter || !filter->is_valid())
159 filter.reset(getFilter("fadeInBrightness", info->producer));
160 if (!filter || !filter->is_valid())
161 filter.reset(getFilter("fadeInMovit", info->producer));
162 if (filter && filter->is_valid() && filter->get(kShotcutAnimInProperty))
163 return filter->get_int(kShotcutAnimInProperty);
164 else
165 return (filter && filter->is_valid())? filter->get_length() : 0;
166 }
167 case FadeOutRole: {
168 QScopedPointer<Mlt::Filter> filter(getFilter("fadeOutVolume", info->producer));
169 if (!filter || !filter->is_valid())
170 filter.reset(getFilter("fadeOutBrightness", info->producer));
171 if (!filter || !filter->is_valid())
172 filter.reset(getFilter("fadeOutMovit", info->producer));
173 if (filter && filter->is_valid() && filter->get(kShotcutAnimOutProperty))
174 return filter->get_int(kShotcutAnimOutProperty);
175 else
176 return (filter && filter->is_valid())? filter->get_length() : 0;
177 }
178 case IsTransitionRole:
179 return isTransition(playlist, index.row());
180 case FileHashRole:
181 return Util::getHash(*info->producer);
182 case SpeedRole: {
183 double speed = 1.0;
184 if (info->producer && info->producer->is_valid()) {
185 if (!qstrcmp("timewarp", info->producer->get("mlt_service")))
186 speed = info->producer->get_double("warp_speed");
187 }
188 return speed;
189 }
190 case IsFilteredRole:
191 return isFiltered(info->producer);
192 case AudioIndexRole:
193 return info->producer->get("audio_index");
194 default:
195 break;
196 }
197 }
198 }
199 else {
200 // Get data for a track.
201 int i = m_trackList.at(index.row()).mlt_index;
202 QScopedPointer<Mlt::Producer> track(m_tractor->track(i));
203 if (track) {
204 Mlt::Playlist playlist(*track);
205 switch (role) {
206 case NameRole:
207 case Qt::DisplayRole:
208 return track->get(kTrackNameProperty);
209 case DurationRole:
210 return playlist.get_playtime();
211 case IsMuteRole:
212 return playlist.get_int("hide") & 2;
213 case IsHiddenRole:
214 return playlist.get_int("hide") & 1;
215 case IsAudioRole:
216 return m_trackList[index.row()].type == AudioTrackType;
217 case IsLockedRole:
218 return track->get_int(kTrackLockProperty);
219 case IsCompositeRole: {
220 QScopedPointer<Mlt::Transition> transition(getTransition("frei0r.cairoblend", i));
221 if (!transition)
222 transition.reset(getTransition("qtblend", i));
223 if (!transition)
224 transition.reset(getTransition("movit.overlay", i));
225 if (transition && transition->is_valid()) {
226 if (!transition->get_int("disable"))
227 return true;
228 }
229 return false;
230 }
231 case IsFilteredRole:
232 return isFiltered(track.data());
233 case IsBottomVideoRole:
234 return m_trackList[index.row()].number == 0 && m_trackList[index.row()].type == VideoTrackType;
235 default:
236 break;
237 }
238 }
239 }
240 return QVariant();
241 }
242
index(int row,int column,const QModelIndex & parent) const243 QModelIndex MultitrackModel::index(int row, int column, const QModelIndex &parent) const
244 {
245 if (column > 0)
246 return QModelIndex();
247 // LOG_DEBUG() << __FUNCTION__ << row << column << parent;
248 QModelIndex result;
249 if (parent.isValid()) {
250 int i = m_trackList.at(parent.row()).mlt_index;
251 QScopedPointer<Mlt::Producer> track(m_tractor->track(i));
252 if (track) {
253 Mlt::Playlist playlist((mlt_playlist) track->get_producer());
254 if (row < playlist.count())
255 result = createIndex(row, column, parent.row());
256 }
257 } else if (row < m_trackList.count()) {
258 result = createIndex(row, column, NO_PARENT_ID);
259 }
260 return result;
261 }
262
makeIndex(int trackIndex,int clipIndex) const263 QModelIndex MultitrackModel::makeIndex(int trackIndex, int clipIndex) const
264 {
265 return index(clipIndex, 0, index(trackIndex));
266 }
267
parent(const QModelIndex & index) const268 QModelIndex MultitrackModel::parent(const QModelIndex &index) const
269 {
270 // LOG_DEBUG() << __FUNCTION__ << index;
271 if (!index.isValid() || index.internalId() == NO_PARENT_ID)
272 return QModelIndex();
273 else
274 return createIndex(index.internalId(), 0, NO_PARENT_ID);
275 }
276
roleNames() const277 QHash<int, QByteArray> MultitrackModel::roleNames() const
278 {
279 QHash<int, QByteArray> roles;
280 roles[NameRole] = "name";
281 roles[ResourceRole] = "resource";
282 roles[ServiceRole] = "mlt_service";
283 roles[IsBlankRole] = "blank";
284 roles[StartRole] = "start";
285 roles[DurationRole] = "duration";
286 roles[InPointRole] = "in";
287 roles[OutPointRole] = "out";
288 roles[FramerateRole] = "fps";
289 roles[IsMuteRole] = "mute";
290 roles[IsHiddenRole] = "hidden";
291 roles[IsAudioRole] = "audio";
292 roles[AudioLevelsRole] = "audioLevels";
293 roles[IsCompositeRole] = "composite";
294 roles[IsLockedRole] = "locked";
295 roles[FadeInRole] = "fadeIn";
296 roles[FadeOutRole] = "fadeOut";
297 roles[IsTransitionRole] = "isTransition";
298 roles[FileHashRole] = "hash";
299 roles[SpeedRole] = "speed";
300 roles[IsFilteredRole] = "filtered";
301 roles[IsBottomVideoRole] = "isBottomVideo";
302 roles[AudioIndexRole] = "audioIndex";
303 return roles;
304 }
305
setTrackName(int row,const QString & value)306 void MultitrackModel::setTrackName(int row, const QString &value)
307 {
308 if (row < m_trackList.size()) {
309 int i = m_trackList.at(row).mlt_index;
310 QScopedPointer<Mlt::Producer> track(m_tractor->track(i));
311 if (track) {
312 track->set(kTrackNameProperty, value.toUtf8().constData());
313
314 QModelIndex modelIndex = index(row, 0);
315 QVector<int> roles;
316 roles << NameRole;
317 emit dataChanged(modelIndex, modelIndex, roles);
318 emit modified();
319 }
320 }
321 }
322
setTrackMute(int row,bool mute)323 void MultitrackModel::setTrackMute(int row, bool mute)
324 {
325 if (row < m_trackList.size()) {
326 int i = m_trackList.at(row).mlt_index;
327 QScopedPointer<Mlt::Producer> track(m_tractor->track(i));
328 if (track) {
329 int hide = track->get_int("hide");
330 if (mute)
331 hide |= 2;
332 else
333 hide ^= 2;
334 track->set("hide", hide);
335
336 QModelIndex modelIndex = index(row, 0);
337 QVector<int> roles;
338 roles << IsMuteRole;
339 emit dataChanged(modelIndex, modelIndex, roles);
340 emit modified();
341 }
342 }
343 }
344
setTrackHidden(int row,bool hidden)345 void MultitrackModel::setTrackHidden(int row, bool hidden)
346 {
347 if (row < m_trackList.size()) {
348 int i = m_trackList.at(row).mlt_index;
349 QScopedPointer<Mlt::Producer> track(m_tractor->track(i));
350 if (track) {
351 int hide = track->get_int("hide");
352 if (hidden)
353 hide |= 1;
354 else
355 hide ^= 1;
356 track->set("hide", hide);
357 MLT.refreshConsumer();
358
359 QModelIndex modelIndex = index(row, 0);
360 QVector<int> roles;
361 roles << IsHiddenRole;
362 emit dataChanged(modelIndex, modelIndex, roles);
363 emit modified();
364 }
365 }
366 }
367
setTrackComposite(int row,bool composite)368 void MultitrackModel::setTrackComposite(int row, bool composite)
369 {
370 if (row < m_trackList.size()) {
371 int i = m_trackList.at(row).mlt_index;
372 QScopedPointer<Mlt::Transition> transition(getTransition("frei0r.cairoblend", i));
373 if (transition) {
374 transition->set("disable", !composite);
375 } else {
376 transition.reset(getTransition("qtblend", i));
377 if (transition) {
378 transition->set("disable", !composite);
379 } else {
380 transition.reset(getTransition("movit.overlay", i));
381 if (transition) {
382 transition->set("disable", !composite);
383 }
384 }
385 }
386 MLT.refreshConsumer();
387
388 QModelIndex modelIndex = index(row, 0);
389 QVector<int> roles;
390 roles << IsCompositeRole;
391 emit dataChanged(modelIndex, modelIndex, roles);
392 emit modified();
393 }
394 }
395
setTrackLock(int row,bool lock)396 void MultitrackModel::setTrackLock(int row, bool lock)
397 {
398 if (row < m_trackList.size()) {
399 int i = m_trackList.at(row).mlt_index;
400 QScopedPointer<Mlt::Producer> track(m_tractor->track(i));
401 track->set(kTrackLockProperty, lock);
402
403 QModelIndex modelIndex = index(row, 0);
404 QVector<int> roles;
405 roles << IsLockedRole;
406 emit dataChanged(modelIndex, modelIndex, roles);
407 emit modified();
408 }
409 }
410
trimClipInValid(int trackIndex,int clipIndex,int delta,bool ripple)411 bool MultitrackModel::trimClipInValid(int trackIndex, int clipIndex, int delta, bool ripple)
412 {
413 bool result = true;
414 int i = m_trackList.at(trackIndex).mlt_index;
415 QScopedPointer<Mlt::Producer> track(m_tractor->track(i));
416 if (track) {
417 Mlt::Playlist playlist(*track);
418 QScopedPointer<Mlt::ClipInfo> info(playlist.clip_info(clipIndex));
419
420 if (!info || (info->frame_in + delta) < 0 || (info->frame_in + delta) > info->frame_out)
421 result = false;
422 else if (delta < 0 && clipIndex <= 0)
423 result = false;
424 else if (!ripple && delta < 0 && clipIndex > 0 && !playlist.is_blank(clipIndex - 1))
425 result = false;
426 else if (!ripple && delta > 0 && clipIndex > 0 && isTransition(playlist, clipIndex - 1))
427 result = false;
428 }
429 return result;
430 }
431
trimClipIn(int trackIndex,int clipIndex,int delta,bool ripple,bool rippleAllTracks)432 int MultitrackModel::trimClipIn(int trackIndex, int clipIndex, int delta, bool ripple, bool rippleAllTracks)
433 {
434 int result = clipIndex;
435 QList<int> otherTracksToRipple;
436 int otherTracksPosition = -1;
437
438 for (int i = 0; i < m_trackList.count(); ++i) {
439 int mltIndex = m_trackList.at(i).mlt_index;
440 QScopedPointer<Mlt::Producer> track(m_tractor->track(mltIndex));
441 if (!track)
442 continue;
443
444 //when not rippling, never touch the other tracks
445 if (trackIndex != i && (!ripple || !rippleAllTracks))
446 continue;
447
448 if (rippleAllTracks) {
449 if (track->get_int(kTrackLockProperty))
450 continue;
451
452 if (trackIndex != i && ripple) {
453 otherTracksToRipple << i;
454 continue;
455 }
456 }
457
458 Mlt::Playlist playlist(*track);
459 QScopedPointer<Mlt::ClipInfo> info(playlist.clip_info(clipIndex));
460 int filterIn = MLT.filterIn(playlist, clipIndex);
461 int filterOut = MLT.filterOut(playlist, clipIndex);
462
463 Q_ASSERT(otherTracksPosition == -1);
464 otherTracksPosition = info->start;
465
466 if (info->frame_in + delta < 0)
467 // clamp to clip start
468 delta = -info->frame_in;
469 if (playlist.is_blank(clipIndex - 1) && -delta > playlist.clip_length(clipIndex - 1))
470 // clamp to duration of blank space
471 delta = -playlist.clip_length(clipIndex - 1);
472 // LOG_DEBUG() << "delta" << delta;
473
474 playlist.resize_clip(clipIndex, info->frame_in + delta, info->frame_out);
475
476 // Adjust filters.
477 adjustClipFilters(*info->producer, filterIn, filterOut, delta, 0);
478
479 QModelIndex modelIndex = createIndex(clipIndex, 0, i);
480 QVector<int> roles;
481 roles << DurationRole;
482 roles << InPointRole;
483 emit dataChanged(modelIndex, modelIndex, roles);
484 AudioLevelsTask::start(*info->producer, this, modelIndex);
485
486 if (!ripple) {
487 // Adjust left of the clip.
488 if (clipIndex > 0 && playlist.is_blank(clipIndex - 1)) {
489 int out = playlist.clip_length(clipIndex - 1) + delta - 1;
490 if (out < 0) {
491 // LOG_DEBUG() << "remove blank at left";
492 beginRemoveRows(index(i), clipIndex - 1, clipIndex - 1);
493 playlist.remove(clipIndex - 1);
494 endRemoveRows();
495 --result;
496 } else {
497 // LOG_DEBUG() << "adjust blank on left to" << out;
498 playlist.resize_clip(clipIndex - 1, 0, out);
499
500 QModelIndex index = createIndex(clipIndex - 1, 0, i);
501 QVector<int> roles;
502 roles << DurationRole;
503 emit dataChanged(index, index, roles);
504 }
505 } else if (delta > 0) {
506 // LOG_DEBUG() << "add blank on left duration" << delta - 1;
507 beginInsertRows(index(i), clipIndex, clipIndex);
508 playlist.insert_blank(clipIndex, delta - 1);
509 endInsertRows();
510 ++result;
511 }
512 }
513 emit modified();
514 }
515 if (delta > 0) {
516 foreach (int idx, otherTracksToRipple) {
517 Q_ASSERT(otherTracksPosition != -1);
518 removeRegion(idx, otherTracksPosition, delta);
519 }
520 } else {
521 insertOrAdjustBlankAt(otherTracksToRipple, otherTracksPosition, -delta);
522 }
523 return result;
524 }
525
notifyClipIn(int trackIndex,int clipIndex)526 void MultitrackModel::notifyClipIn(int trackIndex, int clipIndex)
527 {
528 if (trackIndex >= 0 && trackIndex < m_trackList.size() && clipIndex >= 0) {
529 QModelIndex index = createIndex(clipIndex, 0, trackIndex);
530 QVector<int> roles;
531 roles << AudioLevelsRole;
532 emit dataChanged(index, index, roles);
533 MLT.refreshConsumer();
534 }
535 m_isMakingTransition = false;
536 }
537
trimClipOutValid(int trackIndex,int clipIndex,int delta,bool ripple)538 bool MultitrackModel::trimClipOutValid(int trackIndex, int clipIndex, int delta, bool ripple)
539 {
540 bool result = true;
541 int i = m_trackList.at(trackIndex).mlt_index;
542 QScopedPointer<Mlt::Producer> track(m_tractor->track(i));
543 if (track) {
544 Mlt::Playlist playlist(*track);
545 QScopedPointer<Mlt::ClipInfo> info(playlist.clip_info(clipIndex));
546 if (!info || (info->frame_out - delta) >= info->length || (info->frame_out - delta) < info->frame_in)
547 result = false;
548 else if (!ripple && delta < 0 && (clipIndex + 1) < playlist.count() && !playlist.is_blank(clipIndex + 1))
549 result = false;
550 else if (!ripple && delta > 0 && (clipIndex + 1) < playlist.count() && isTransition(playlist, clipIndex + 1))
551 return false;
552 }
553 return result;
554 }
555
trackHeight() const556 int MultitrackModel::trackHeight() const
557 {
558 int result = m_tractor? m_tractor->get_int(kTrackHeightProperty) : Settings.timelineTrackHeight();
559 return result? result : qMax(10, Settings.timelineTrackHeight());
560 }
561
setTrackHeight(int height)562 void MultitrackModel::setTrackHeight(int height)
563 {
564 if (m_tractor) {
565 Settings.setTimelineTrackHeight(height);
566 m_tractor->set(kTrackHeightProperty, Settings.timelineTrackHeight());
567 emit trackHeightChanged();
568 }
569 }
570
scaleFactor() const571 double MultitrackModel::scaleFactor() const
572 {
573 double result = m_tractor? m_tractor->get_double(kTimelineScaleProperty) : 0;
574 return (result > 0)? result : (qPow(1.0, 3.0) + 0.01);
575 }
576
setScaleFactor(double scale)577 void MultitrackModel::setScaleFactor(double scale)
578 {
579 if (m_tractor) {
580 m_tractor->set(kTimelineScaleProperty, scale);
581 emit scaleFactorChanged();
582 }
583 }
584
trimClipOut(int trackIndex,int clipIndex,int delta,bool ripple,bool rippleAllTracks)585 int MultitrackModel::trimClipOut(int trackIndex, int clipIndex, int delta, bool ripple, bool rippleAllTracks)
586 {
587 QList<int> otherTracksToRipple;
588 int result = clipIndex;
589 int otherTracksPosition = -1;
590
591 for (int i = 0; i < m_trackList.count(); ++i) {
592 int mltIndex = m_trackList.at(i).mlt_index;
593 QScopedPointer<Mlt::Producer> track(m_tractor->track(mltIndex));
594 if (!track)
595 continue;
596
597 Mlt::Playlist playlist(*track);
598 QScopedPointer<Mlt::ClipInfo> info(playlist.clip_info(clipIndex));
599 int filterIn = MLT.filterIn(playlist, clipIndex);
600 int filterOut = MLT.filterOut(playlist, clipIndex);
601
602 //when not rippling, never touch the other tracks
603 if (trackIndex != i && (!ripple || !rippleAllTracks))
604 continue;
605
606 if (rippleAllTracks) {
607 if (track->get_int(kTrackLockProperty))
608 continue;
609
610 if (trackIndex != i && ripple) {
611 otherTracksToRipple << i;
612 continue;
613 }
614 }
615
616 Q_ASSERT(otherTracksPosition == -1);
617 otherTracksPosition = info->start + info->frame_count - delta;
618
619 if ((info->frame_out - delta) >= info->length)
620 // clamp to clip duration
621 delta = info->frame_out - info->length + 1;
622 if ((clipIndex + 1) < playlist.count() && playlist.is_blank(clipIndex + 1) && -delta > playlist.clip_length(clipIndex + 1))
623 delta = -playlist.clip_length(clipIndex + 1);
624 // LOG_DEBUG() << "delta" << delta;
625
626 if (!ripple) {
627 // Adjust right of the clip.
628 if (clipIndex >= 0 && (clipIndex + 1) < playlist.count() && playlist.is_blank(clipIndex + 1)) {
629 int out = playlist.clip_length(clipIndex + 1) + delta - 1;
630 if (out < 0) {
631 // LOG_DEBUG() << "remove blank at right";
632 beginRemoveRows(index(i), clipIndex + 1, clipIndex + 1);
633 playlist.remove(clipIndex + 1);
634 endRemoveRows();
635 } else {
636 // LOG_DEBUG() << "adjust blank on right to" << out;
637 playlist.resize_clip(clipIndex + 1, 0, out);
638
639 QModelIndex index = createIndex(clipIndex + 1, 0, i);
640 QVector<int> roles;
641 roles << DurationRole;
642 emit dataChanged(index, index, roles);
643 }
644 } else if (delta > 0 && (clipIndex + 1) < playlist.count()) {
645 // Add blank to right.
646 // LOG_DEBUG() << "add blank on right duration" << (delta - 1);
647 int newIndex = clipIndex + 1;
648 beginInsertRows(index(i), newIndex, newIndex);
649 playlist.insert_blank(newIndex, delta - 1);
650 endInsertRows();
651 }
652 }
653 playlist.resize_clip(clipIndex, info->frame_in, info->frame_out - delta);
654
655 // Adjust filters.
656 adjustClipFilters(*info->producer, filterIn, filterOut, 0, delta);
657
658 QModelIndex index = createIndex(clipIndex, 0, i);
659 QVector<int> roles;
660 roles << DurationRole;
661 roles << OutPointRole;
662 emit dataChanged(index, index, roles);
663 AudioLevelsTask::start(*info->producer, this, index);
664 emit modified();
665 }
666 if (delta > 0) {
667 foreach (int idx, otherTracksToRipple) {
668 Q_ASSERT(otherTracksPosition != -1);
669 removeRegion(idx, otherTracksPosition, delta);
670 }
671 } else {
672 insertOrAdjustBlankAt(otherTracksToRipple, otherTracksPosition, -delta);
673 }
674 return result;
675 }
676
notifyClipOut(int trackIndex,int clipIndex)677 void MultitrackModel::notifyClipOut(int trackIndex, int clipIndex)
678 {
679 if (trackIndex >= 0 && trackIndex < m_trackList.size() && clipIndex >= 0) {
680 QModelIndex index = createIndex(clipIndex, 0, trackIndex);
681 QVector<int> roles;
682 roles << AudioLevelsRole;
683 emit dataChanged(index, index, roles);
684 MLT.refreshConsumer();
685 }
686 m_isMakingTransition = false;
687 }
688
moveClip(int fromTrack,int toTrack,int clipIndex,int position,bool ripple,bool rippleAllTracks)689 bool MultitrackModel::moveClip(int fromTrack, int toTrack, int clipIndex,
690 int position, bool ripple, bool rippleAllTracks)
691 {
692 // LOG_DEBUG() << __FUNCTION__ << clipIndex << "fromTrack" << fromTrack << "toTrack" << toTrack;
693 bool result = false;
694 int i = m_trackList.at(fromTrack).mlt_index;
695 QScopedPointer<Mlt::Producer> track(m_tractor->track(i));
696
697 if (track) {
698 Mlt::Playlist playlist(*track);
699 QScopedPointer<Mlt::ClipInfo> info(playlist.clip_info(clipIndex));
700 QString xml = MLT.XML(info->producer);
701 Mlt::Producer clip(MLT.profile(), "xml-string", xml.toUtf8().constData());
702
703 if (clip.is_valid()) {
704 clearMixReferences(fromTrack, clipIndex);
705 clip.set_in_and_out(info->frame_in, info->frame_out);
706
707 if (ripple) {
708 int targetIndex = playlist.get_clip_index_at(position);
709 int length = playlist.clip_length(clipIndex);
710 int targetIndexEnd = playlist.get_clip_index_at(position + length - 1);
711 if ((clipIndex + 1) < playlist.count() && position >= playlist.get_playtime()) {
712 // Clip relocated to end of playlist.
713 moveClipToEnd(playlist, toTrack, clipIndex, position, ripple, rippleAllTracks);
714 emit modified();
715 }
716 else if (fromTrack == toTrack && targetIndex >= clipIndex) {
717 // Push the clips.
718 int clipStart = playlist.clip_start(clipIndex);
719 int duration = position - clipStart;
720 QList<int> trackList;
721 trackList << fromTrack;
722 if (rippleAllTracks) {
723 for (int i = 0; i < m_trackList.count(); ++i) {
724 if (i == fromTrack)
725 continue;
726 int mltIndex = m_trackList.at(i).mlt_index;
727 QScopedPointer<Mlt::Producer> otherTrack(m_tractor->track(mltIndex));
728 if (otherTrack && otherTrack->get_int(kTrackLockProperty))
729 continue;
730 trackList << i;
731 }
732 }
733 insertOrAdjustBlankAt(trackList, clipStart, duration);
734 consolidateBlanks(playlist, fromTrack);
735 emit modified();
736 } else if (fromTrack == toTrack && (playlist.is_blank_at(position) || targetIndex == clipIndex) &&
737 (playlist.is_blank_at(position + length - 1) || targetIndexEnd == clipIndex)) {
738 // Reposition the clip within its current blank spot.
739 moveClipInBlank(playlist, toTrack, clipIndex, position, ripple, rippleAllTracks);
740 emit modified();
741 } else {
742 int clipPlaytime = clip.get_playtime();
743 int clipStart = playlist.clip_start(clipIndex);
744
745 // Remove clip
746 clearMixReferences(fromTrack, clipIndex);
747 beginRemoveRows(index(fromTrack), clipIndex, clipIndex);
748 playlist.remove(clipIndex);
749 endRemoveRows();
750 consolidateBlanks(playlist, fromTrack);
751
752 // Ripple delete on all unlocked tracks.
753 if (clipPlaytime > 0 && rippleAllTracks)
754 for (int j = 0; j < m_trackList.count(); ++j) {
755 if (j == fromTrack)
756 continue;
757
758 int mltIndex = m_trackList.at(j).mlt_index;
759 QScopedPointer<Mlt::Producer> otherTrack(m_tractor->track(mltIndex));
760 if (otherTrack) {
761 if (otherTrack->get_int(kTrackLockProperty))
762 continue;
763
764 removeRegion(j, clipStart, clipPlaytime);
765 }
766 }
767 consolidateBlanks(playlist, fromTrack);
768
769 // Insert clip
770 insertClip(toTrack, clip, position, rippleAllTracks, false);
771 }
772 } else {
773 // Lift clip
774 delete playlist.replace_with_blank(clipIndex);
775
776 QModelIndex index = createIndex(clipIndex, 0, fromTrack);
777 QVector<int> roles;
778 roles << ResourceRole;
779 roles << ServiceRole;
780 roles << IsBlankRole;
781 roles << IsTransitionRole;
782 emit dataChanged(index, index, roles);
783
784 consolidateBlanks(playlist, fromTrack);
785
786 // Overwrite with clip
787 if (position + clip.get_playtime() >= 0)
788 overwrite(toTrack, clip, position, false /* seek */);
789 else
790 emit modified();
791 }
792 }
793 result = true;
794 }
795 return result;
796 }
797
overwriteClip(int trackIndex,Mlt::Producer & clip,int position,bool seek)798 int MultitrackModel::overwriteClip(int trackIndex, Mlt::Producer& clip, int position, bool seek)
799 {
800 createIfNeeded();
801 int result = -1;
802 int i = m_trackList.at(trackIndex).mlt_index;
803 QScopedPointer<Mlt::Producer> track(m_tractor->track(i));
804 if (track) {
805 Mlt::Playlist playlist(*track);
806 if (position >= playlist.get_playtime() - 1) {
807 // LOG_DEBUG() << __FUNCTION__ << "appending";
808 removeBlankPlaceholder(playlist, trackIndex);
809 int n = playlist.count();
810 int length = position - playlist.clip_start(n - 1) - playlist.clip_length(n - 1);
811
812 // Add blank to end if needed.
813 if (length > 0) {
814 beginInsertRows(index(trackIndex), n, n);
815 playlist.blank(length - 1);
816 endInsertRows();
817 ++n;
818 }
819
820 // Append clip.
821 int in = clip.get_in();
822 int out = clip.get_out();
823 clip.set_in_and_out(0, clip.get_length() - 1);
824 beginInsertRows(index(trackIndex), n, n);
825 playlist.append(clip.parent(), in, out);
826 endInsertRows();
827 AudioLevelsTask::start(clip.parent(), this, createIndex(n, 0, trackIndex));
828 result = playlist.count() - 1;
829 } else if (position + clip.get_playtime() > playlist.get_playtime()
830 // Handle straddling - new clip larger than another with blanks on both sides.
831 || playlist.get_clip_index_at(position) == playlist.get_clip_index_at(position + clip.get_playtime() - 1)) {
832 // LOG_DEBUG() << __FUNCTION__ << "overwriting blank space" << clip.get_playtime();
833 int targetIndex = playlist.get_clip_index_at(position);
834
835 if (position > playlist.clip_start(targetIndex)) {
836 splitClip(trackIndex, targetIndex, position);
837
838 // Notify item on left was adjusted.
839 QModelIndex modelIndex = createIndex(targetIndex, 0, trackIndex);
840 QVector<int> roles;
841 roles << DurationRole;
842 emit dataChanged(modelIndex, modelIndex, roles);
843 AudioLevelsTask::start(clip.parent(), this, modelIndex);
844 ++targetIndex;
845 } else if (position < 0) {
846 clip.set_in_and_out(-position, clip.get_out());
847 QModelIndex modelIndex = createIndex(targetIndex, 0, trackIndex);
848 // Notify clip on right was adjusted.
849 QVector<int> roles;
850 roles << InPointRole;
851 roles << DurationRole;
852 emit dataChanged(modelIndex, modelIndex, roles);
853 }
854
855 // Adjust clip on right.
856 int duration = playlist.clip_length(targetIndex) - clip.get_playtime();
857 if (duration > 0) {
858 // LOG_DEBUG() << "adjust item on right" << (targetIndex) << " to" << duration;
859 playlist.resize_clip(targetIndex, 0, duration - 1);
860 QModelIndex modelIndex = createIndex(targetIndex, 0, trackIndex);
861 // Notify clip on right was adjusted.
862 QVector<int> roles;
863 roles << DurationRole;
864 emit dataChanged(modelIndex, modelIndex, roles);
865 AudioLevelsTask::start(clip.parent(), this, modelIndex);
866 } else {
867 // LOG_DEBUG() << "remove item on right";
868 clearMixReferences(trackIndex, targetIndex);
869 beginRemoveRows(index(trackIndex), targetIndex, targetIndex);
870 playlist.remove(targetIndex);
871 endRemoveRows();
872 }
873 // Insert clip between subclips.
874 int in = clip.get_in();
875 int out = clip.get_out();
876 clip.set_in_and_out(0, clip.get_length() - 1);
877 beginInsertRows(index(trackIndex), targetIndex, targetIndex);
878 playlist.insert(clip.parent(), targetIndex, in, out);
879 endInsertRows();
880 result = targetIndex;
881 }
882 if (result >= 0) {
883 QModelIndex index = createIndex(result, 0, trackIndex);
884 AudioLevelsTask::start(clip.parent(), this, index);
885 emit modified();
886 if (seek)
887 emit seeked(playlist.clip_start(result) + playlist.clip_length(result));
888 }
889 }
890 return result;
891 }
892
overwrite(int trackIndex,Mlt::Producer & clip,int position,bool seek)893 QString MultitrackModel::overwrite(int trackIndex, Mlt::Producer& clip, int position, bool seek)
894 {
895 createIfNeeded();
896 Mlt::Playlist result;
897 int i = m_trackList.at(trackIndex).mlt_index;
898 QScopedPointer<Mlt::Producer> track(m_tractor->track(i));
899 if (track) {
900 Mlt::Playlist playlist(*track);
901 removeBlankPlaceholder(playlist, trackIndex);
902 int targetIndex = playlist.get_clip_index_at(position);
903 if (position >= playlist.get_playtime() - 1) {
904 // LOG_DEBUG() << __FUNCTION__ << "appending";
905 int n = playlist.count();
906 int length = position - playlist.clip_start(n - 1) - playlist.clip_length(n - 1);
907
908 // Add blank to end if needed.
909 if (length > 0) {
910 beginInsertRows(index(trackIndex), n, n);
911 playlist.blank(length - 1);
912 endInsertRows();
913 ++n;
914 }
915
916 // Append clip.
917 int in = clip.get_in();
918 int out = clip.get_out();
919 clip.set_in_and_out(0, clip.get_length() - 1);
920 beginInsertRows(index(trackIndex), n, n);
921 playlist.append(clip.parent(), in, out);
922 endInsertRows();
923 targetIndex = playlist.count() - 1;
924 } else {
925 int lastIndex = playlist.get_clip_index_at(position + clip.get_playtime());
926 // LOG_DEBUG() << __FUNCTION__ << "overwriting with duration" << clip.get_playtime()
927 // << "from" << targetIndex << "to" << lastIndex;
928
929 // Add affected clips to result playlist.
930 int i = targetIndex;
931 if (position == playlist.clip_start(targetIndex))
932 --i;
933 for (; i <= lastIndex; i++) {
934 Mlt::Producer* producer = playlist.get_clip(i);
935 if (producer)
936 result.append(*producer);
937 delete producer;
938 }
939
940 if (position > playlist.clip_start(targetIndex)) {
941 // LOG_DEBUG() << "split starting item" << targetIndex;
942 splitClip(trackIndex, targetIndex, position);
943 ++targetIndex;
944 } else if (position < 0) {
945 clip.set_in_and_out(clip.get_in() - position, clip.get_out());
946 position = 0;
947 QModelIndex modelIndex = createIndex(targetIndex, 0, trackIndex);
948 // Notify clip was adjusted.
949 QVector<int> roles;
950 roles << InPointRole;
951 roles << DurationRole;
952 emit dataChanged(modelIndex, modelIndex, roles);
953 }
954
955 int length = clip.get_playtime();
956 while (length > 0 && targetIndex < playlist.count()) {
957 if (playlist.clip_length(targetIndex) > length) {
958 // LOG_DEBUG() << "split last item" << targetIndex;
959 splitClip(trackIndex, targetIndex, position + length);
960 }
961 // LOG_DEBUG() << "length" << length << "item length" << playlist.clip_length(targetIndex);
962 length -= playlist.clip_length(targetIndex);
963 // LOG_DEBUG() << "delete item" << targetIndex;
964 clearMixReferences(trackIndex, targetIndex);
965 beginRemoveRows(index(trackIndex), targetIndex, targetIndex);
966 playlist.remove(targetIndex);
967 endRemoveRows();
968 }
969
970 // Insert clip between subclips.
971 int in = clip.get_in();
972 int out = clip.get_out();
973 clip.set_in_and_out(0, clip.get_length() - 1);
974 beginInsertRows(index(trackIndex), targetIndex, targetIndex);
975 playlist.insert(clip.parent(), targetIndex, in, out);
976 endInsertRows();
977 }
978 QModelIndex index = createIndex(targetIndex, 0, trackIndex);
979 AudioLevelsTask::start(clip.parent(), this, index);
980 emit overWritten(trackIndex, targetIndex);
981 emit modified();
982 emit seeked(playlist.clip_start(targetIndex) + playlist.clip_length(targetIndex), seek);
983 }
984 return MLT.XML(&result);
985 }
986
insertClip(int trackIndex,Mlt::Producer & clip,int position,bool rippleAllTracks,bool seek)987 int MultitrackModel::insertClip(int trackIndex, Mlt::Producer &clip, int position, bool rippleAllTracks, bool seek)
988 {
989 createIfNeeded();
990 int result = -1;
991 int i = m_trackList.at(trackIndex).mlt_index;
992 int clipPlaytime = clip.get_playtime();
993 QScopedPointer<Mlt::Producer> track(m_tractor->track(i));
994 if (track) {
995 Mlt::Playlist playlist(*track);
996 if (position >= playlist.get_playtime() - 1) {
997 // LOG_DEBUG() << __FUNCTION__ << "appending";
998 removeBlankPlaceholder(playlist, trackIndex);
999 int n = playlist.count();
1000 int length = position - playlist.clip_start(n - 1) - playlist.clip_length(n - 1);
1001
1002 // Add blank to end if needed.
1003 if (length > 0) {
1004 beginInsertRows(index(trackIndex), n, n);
1005 playlist.blank(length - 1);
1006 endInsertRows();
1007 ++n;
1008 }
1009
1010 // Append clip.
1011 int in = clip.get_in();
1012 int out = clip.get_out();
1013 clip.set_in_and_out(0, clip.get_length() - 1);
1014 beginInsertRows(index(trackIndex), n, n);
1015 playlist.append(clip.parent(), in, out);
1016 endInsertRows();
1017 result = playlist.count() - 1;
1018 } else {
1019 // LOG_DEBUG() << __FUNCTION__ << "inserting" << position << MLT.XML(&clip);
1020 int targetIndex = playlist.get_clip_index_at(position);
1021
1022 if (position > playlist.clip_start(targetIndex)) {
1023 splitClip(trackIndex, targetIndex, position);
1024
1025 // Notify item on left was adjusted.
1026 QModelIndex modelIndex = createIndex(targetIndex, 0, trackIndex);
1027 QVector<int> roles;
1028 roles << DurationRole;
1029 emit dataChanged(modelIndex, modelIndex, roles);
1030 AudioLevelsTask::start(clip.parent(), this, modelIndex);
1031 ++targetIndex;
1032
1033 // Notify item on right was adjusted.
1034 modelIndex = createIndex(targetIndex, 0, trackIndex);
1035 emit dataChanged(modelIndex, modelIndex, roles);
1036 AudioLevelsTask::start(clip.parent(), this, modelIndex);
1037 } else if (position < 0) {
1038 clip.set_in_and_out(clip.get_in() - position, clip.get_out());
1039 position = 0;
1040 QModelIndex modelIndex = createIndex(targetIndex, 0, trackIndex);
1041 // Notify clip was adjusted.
1042 QVector<int> roles;
1043 roles << InPointRole;
1044 roles << DurationRole;
1045 emit dataChanged(modelIndex, modelIndex, roles);
1046 }
1047
1048 // Insert clip between split blanks.
1049 beginInsertRows(index(trackIndex), targetIndex, targetIndex);
1050 if (qstrcmp("blank", clip.get("mlt_service"))) {
1051 int in = clip.get_in();
1052 int out = clip.get_out();
1053 clip.set_in_and_out(0, clip.get_length() - 1);
1054 playlist.insert(clip.parent(), targetIndex, in, out);
1055 } else {
1056 playlist.insert_blank(targetIndex, clipPlaytime - 1);
1057 }
1058 endInsertRows();
1059 result = targetIndex;
1060 }
1061 if (result >= 0) {
1062 if (rippleAllTracks) {
1063 //fill in/expand blanks in all the other tracks
1064 QList<int> tracksToInsertBlankInto;
1065 for (int j = 0; j < m_trackList.count(); ++j) {
1066 if (j == trackIndex)
1067 continue;
1068 int mltIndex = m_trackList.at(j).mlt_index;
1069 QScopedPointer<Mlt::Producer> otherTrack(m_tractor->track(mltIndex));
1070 if (otherTrack->get_int(kTrackLockProperty))
1071 continue;
1072
1073 tracksToInsertBlankInto << j;
1074 }
1075 if (!tracksToInsertBlankInto.isEmpty())
1076 insertOrAdjustBlankAt(tracksToInsertBlankInto, position, clipPlaytime);
1077 }
1078
1079 QModelIndex index = createIndex(result, 0, trackIndex);
1080 AudioLevelsTask::start(clip.parent(), this, index);
1081 emit inserted(trackIndex, result);
1082 emit modified();
1083 emit seeked(playlist.clip_start(result) + playlist.clip_length(result), seek);
1084 }
1085 }
1086 return result;
1087 }
1088
appendClip(int trackIndex,Mlt::Producer & clip)1089 int MultitrackModel::appendClip(int trackIndex, Mlt::Producer &clip)
1090 {
1091 if (!createIfNeeded()) {
1092 return -1;
1093 }
1094 int i = m_trackList.at(trackIndex).mlt_index;
1095 QScopedPointer<Mlt::Producer> track(m_tractor->track(i));
1096 if (track) {
1097 Mlt::Playlist playlist(*track);
1098 removeBlankPlaceholder(playlist, trackIndex);
1099 i = playlist.count();
1100 int in = clip.get_in();
1101 int out = clip.get_out();
1102 clip.set_in_and_out(0, clip.get_length() - 1);
1103 beginInsertRows(index(trackIndex), i, i);
1104 playlist.append(clip.parent(), in, out);
1105 endInsertRows();
1106 QModelIndex index = createIndex(i, 0, trackIndex);
1107 AudioLevelsTask::start(clip.parent(), this, index);
1108 emit appended(trackIndex, i);
1109 emit modified();
1110 emit seeked(playlist.clip_start(i) + playlist.clip_length(i));
1111 return i;
1112 }
1113 return -1;
1114 }
1115
removeClip(int trackIndex,int clipIndex,bool rippleAllTracks)1116 void MultitrackModel::removeClip(int trackIndex, int clipIndex, bool rippleAllTracks)
1117 {
1118 int i = m_trackList.at(trackIndex).mlt_index;
1119 QScopedPointer<Mlt::Producer> track(m_tractor->track(i));
1120 int clipPlaytime = -1;
1121 int clipStart = -1;
1122
1123 if (track) {
1124 Mlt::Playlist playlist(*track);
1125 if (clipIndex < playlist.count()) {
1126 // Shotcut does not like the behavior of remove() on a
1127 // transition (MLT mix clip). So, we null mlt_mix to prevent it.
1128 clearMixReferences(trackIndex, clipIndex);
1129
1130 QScopedPointer<Mlt::Producer> producer(playlist.get_clip(clipIndex));
1131 if (producer) {
1132 clipPlaytime = producer->get_playtime();
1133 clipStart = playlist.clip_start(clipIndex);
1134 }
1135
1136 beginRemoveRows(index(trackIndex), clipIndex, clipIndex);
1137 playlist.remove(clipIndex);
1138 endRemoveRows();
1139 consolidateBlanks(playlist, trackIndex);
1140
1141 // Ripple all unlocked tracks.
1142 if (clipPlaytime > 0 && rippleAllTracks)
1143 for (int j = 0; j < m_trackList.count(); ++j) {
1144 if (j == trackIndex)
1145 continue;
1146
1147 int mltIndex = m_trackList.at(j).mlt_index;
1148 QScopedPointer<Mlt::Producer> otherTrack(m_tractor->track(mltIndex));
1149 if (otherTrack) {
1150 if (otherTrack->get_int(kTrackLockProperty))
1151 continue;
1152
1153 removeRegion(j, clipStart, clipPlaytime);
1154 }
1155 }
1156 consolidateBlanks(playlist, trackIndex);
1157 emit modified();
1158 }
1159 }
1160 }
1161
liftClip(int trackIndex,int clipIndex)1162 void MultitrackModel::liftClip(int trackIndex, int clipIndex)
1163 {
1164 int i = m_trackList.at(trackIndex).mlt_index;
1165 QScopedPointer<Mlt::Producer> track(m_tractor->track(i));
1166 if (track) {
1167 Mlt::Playlist playlist(*track);
1168 if (clipIndex < playlist.count()) {
1169 // Shotcut does not like the behavior of replace_with_blank() on a
1170 // transition (MLT mix clip). So, we null mlt_mix to prevent it.
1171 clearMixReferences(trackIndex, clipIndex);
1172
1173 delete playlist.replace_with_blank(clipIndex);
1174
1175 QModelIndex index = createIndex(clipIndex, 0, trackIndex);
1176 QVector<int> roles;
1177 roles << ResourceRole;
1178 roles << ServiceRole;
1179 roles << IsBlankRole;
1180 roles << IsTransitionRole;
1181 emit dataChanged(index, index, roles);
1182
1183 consolidateBlanks(playlist, trackIndex);
1184
1185 emit modified();
1186 }
1187 }
1188 }
1189
splitClip(int trackIndex,int clipIndex,int position)1190 void MultitrackModel::splitClip(int trackIndex, int clipIndex, int position)
1191 {
1192 int i = m_trackList.at(trackIndex).mlt_index;
1193 QScopedPointer<Mlt::Producer> track(m_tractor->track(i));
1194 if (track) {
1195 Mlt::Playlist playlist(*track);
1196 QScopedPointer<Mlt::ClipInfo> info(playlist.clip_info(clipIndex));
1197
1198 // Make copy of clip.
1199 Mlt::Producer producer(MLT.profile(), "xml-string",
1200 MLT.XML(info->producer).toUtf8().constData());
1201 int in = info->frame_in;
1202 int out = info->frame_out;
1203 int filterIn = MLT.filterIn(playlist, clipIndex);
1204 int filterOut = MLT.filterOut(playlist, clipIndex);
1205 int duration = position - playlist.clip_start(clipIndex);
1206 int delta = info->frame_count - duration;
1207
1208 // Connect a transition on the left to the new producer.
1209 if (isTransition(playlist, clipIndex - 1) && !playlist.is_blank(clipIndex)) {
1210 QScopedPointer<Mlt::Producer> p(playlist.get_clip(clipIndex - 1));
1211 Mlt::Tractor tractor(p->parent());
1212 if (tractor.is_valid()) {
1213 QScopedPointer<Mlt::Producer> track_b(tractor.track(1));
1214 track_b.reset(producer.cut(track_b->get_in(), track_b->get_out()));
1215 tractor.set_track(*track_b, 1);
1216 }
1217 }
1218
1219 // Remove fades that are usually not desired after split.
1220 QScopedPointer<Mlt::Filter> filter(getFilter("fadeOutVolume", &producer));
1221 if (filter && filter->is_valid())
1222 producer.detach(*filter);
1223 filter.reset(getFilter("fadeOutBrightness", &producer));
1224 if (filter && filter->is_valid())
1225 producer.detach(*filter);
1226 filter.reset(getFilter("fadeOutMovit", &producer));
1227 if (filter && filter->is_valid())
1228 producer.detach(*filter);
1229 filter.reset(getFilter("fadeInVolume", info->producer));
1230 if (filter && filter->is_valid())
1231 info->producer->detach(*filter);
1232 filter.reset(getFilter("fadeInBrightness", info->producer));
1233 if (filter && filter->is_valid())
1234 info->producer->detach(*filter);
1235 filter.reset(getFilter("fadeInMovit", info->producer));
1236 if (filter && filter->is_valid())
1237 info->producer->detach(*filter);
1238
1239 beginInsertRows(index(trackIndex), clipIndex, clipIndex);
1240 if (playlist.is_blank(clipIndex)) {
1241 playlist.insert_blank(clipIndex, duration - 1);
1242 } else {
1243 playlist.insert(producer, clipIndex, in, in + duration - 1);
1244 QModelIndex modelIndex = createIndex(clipIndex, 0, trackIndex);
1245 AudioLevelsTask::start(producer.parent(), this, modelIndex);
1246 }
1247 endInsertRows();
1248
1249 adjustClipFilters(producer, filterIn, out, 0, delta);
1250
1251 playlist.resize_clip(clipIndex + 1, in + duration, out);
1252 QModelIndex modelIndex = createIndex(clipIndex + 1, 0, trackIndex);
1253 QVector<int> roles;
1254 roles << DurationRole;
1255 roles << InPointRole;
1256 roles << FadeInRole;
1257 emit dataChanged(modelIndex, modelIndex, roles);
1258 AudioLevelsTask::start(*info->producer, this, modelIndex);
1259
1260 delta = duration;
1261 adjustClipFilters(*info->producer, in, filterOut, delta, 0);
1262
1263 emit modified();
1264 }
1265 }
1266
joinClips(int trackIndex,int clipIndex)1267 void MultitrackModel::joinClips(int trackIndex, int clipIndex)
1268 {
1269 if (clipIndex < 0) return;
1270 int i = m_trackList.at(trackIndex).mlt_index;
1271 QScopedPointer<Mlt::Producer> track(m_tractor->track(i));
1272 if (track) {
1273 Mlt::Playlist playlist(*track);
1274 if (clipIndex >= playlist.count() - 1) return;
1275 QScopedPointer<Mlt::ClipInfo> info(playlist.clip_info(clipIndex));
1276 int in = info->frame_in;
1277 int out = info->frame_out;
1278 int delta = -playlist.clip_length(clipIndex + 1);
1279
1280 // Move a fade out on the right clip onto the left clip.
1281 QScopedPointer<Mlt::Producer> clip(playlist.get_clip(clipIndex));
1282 info.reset(playlist.clip_info(clipIndex + 1));
1283 QScopedPointer<Mlt::Filter> filter(getFilter("fadeOutVolume", info->producer));
1284 if (filter && filter->is_valid())
1285 clip->parent().attach(*filter);
1286 filter.reset(getFilter("fadeOutBrightness", info->producer));
1287 if (filter && filter->is_valid())
1288 clip->parent().attach(*filter);
1289 filter.reset(getFilter("fadeOutMovit", info->producer));
1290 if (filter && filter->is_valid())
1291 clip->parent().attach(*filter);
1292
1293 playlist.resize_clip(clipIndex, in, out - delta);
1294 QModelIndex modelIndex = createIndex(clipIndex, 0, trackIndex);
1295 QVector<int> roles;
1296 roles << DurationRole;
1297 roles << OutPointRole;
1298 roles << FadeOutRole;
1299 emit dataChanged(modelIndex, modelIndex, roles);
1300 AudioLevelsTask::start(clip->parent(), this, modelIndex);
1301
1302 clearMixReferences(trackIndex, clipIndex + 1);
1303 beginRemoveRows(index(trackIndex), clipIndex + 1, clipIndex + 1);
1304 playlist.remove(clipIndex + 1);
1305 endRemoveRows();
1306
1307 adjustClipFilters(clip->parent(), in, out, 0, delta);
1308
1309 emit modified();
1310 }
1311 }
1312
fadeIn(int trackIndex,int clipIndex,int duration)1313 void MultitrackModel::fadeIn(int trackIndex, int clipIndex, int duration)
1314 {
1315 int i = m_trackList.at(trackIndex).mlt_index;
1316 QScopedPointer<Mlt::Producer> track(m_tractor->track(i));
1317 if (track) {
1318 Mlt::Playlist playlist(*track);
1319 QScopedPointer<Mlt::ClipInfo> info(playlist.clip_info(clipIndex));
1320 if (info && info->producer && info->producer->is_valid()) {
1321 bool isChanged = false;
1322 QScopedPointer<Mlt::Filter> filter;
1323 duration = qBound(0, duration, info->frame_count);
1324
1325 if (m_trackList[trackIndex].type == VideoTrackType
1326 // If video track index is not None.
1327 && (!info->producer->get("video_index") || info->producer->get_int("video_index") != -1)) {
1328
1329 // Get video filter.
1330 if (Settings.playerGPU())
1331 filter.reset(getFilter("fadeInMovit", info->producer));
1332 else
1333 filter.reset(getFilter("fadeInBrightness", info->producer));
1334
1335 if (duration > 0) {
1336 // Add video filter if needed.
1337 if (!filter) {
1338 if (Settings.playerGPU()) {
1339 Mlt::Filter f(MLT.profile(), "movit.opacity");
1340 f.set(kShotcutFilterProperty, "fadeInMovit");
1341 f.set("alpha", 1);
1342 info->producer->attach(f);
1343 filter.reset(new Mlt::Filter(f));
1344 } else {
1345 Mlt::Filter f(MLT.profile(), "brightness");
1346 f.set(kShotcutFilterProperty, "fadeInBrightness");
1347 f.set("alpha", 1);
1348 info->producer->attach(f);
1349 filter.reset(new Mlt::Filter(f));
1350 }
1351 filter->set_in_and_out(info->frame_in, info->frame_out);
1352 emit filterOutChanged(info->frame_out, filter.data());
1353 }
1354
1355 // Adjust video filter.
1356 if (Settings.playerGPU()) {
1357 // Special handling for animation keyframes on movit.opacity.
1358 filter->clear("opacity");
1359 filter->anim_set("opacity", 0, 0, 0, mlt_keyframe_smooth);
1360 filter->anim_set("opacity", 1, duration - 1);
1361 } else {
1362 // Special handling for animation keyframes on brightness.
1363 const char* key = filter->get_int("alpha") != 1? "alpha" : "level";
1364 filter->clear(key);
1365 filter->anim_set(key, 0, 0);
1366 filter->anim_set(key, 1, duration - 1);
1367 }
1368 filter->set(kShotcutAnimInProperty, duration);
1369 isChanged = true;
1370 } else if (filter) {
1371 // Remove the video filter.
1372 info->producer->detach(*filter);
1373 emit filterAddedOrRemoved(info->producer);
1374 filter->set(kShotcutAnimInProperty, duration);
1375 isChanged = true;
1376 }
1377 }
1378
1379 // If audio track index is not None.
1380 if (!info->producer->get("audio_index") || info->producer->get_int("audio_index") != -1) {
1381 // Get audio filter.
1382 filter.reset(getFilter("fadeInVolume", info->producer));
1383
1384 if (duration > 0) {
1385 // Add audio filter if needed.
1386 if (!filter) {
1387 Mlt::Filter f(MLT.profile(), "volume");
1388 f.set(kShotcutFilterProperty, "fadeInVolume");
1389 info->producer->attach(f);
1390 filter.reset(new Mlt::Filter(f));
1391 filter->set_in_and_out(info->frame_in, info->frame_out);
1392 emit filterOutChanged(info->frame_out, filter.data());
1393 }
1394
1395 // Adjust audio filter.
1396 filter->clear("level");
1397 filter->anim_set("level", -60, 0);
1398 filter->anim_set("level", 0, duration - 1);
1399 filter->set(kShotcutAnimInProperty, duration);
1400 isChanged = true;
1401 } else if (filter) {
1402 // Remove the audio filter.
1403 info->producer->detach(*filter);
1404 emit filterAddedOrRemoved(info->producer);
1405 filter->set(kShotcutAnimInProperty, duration);
1406 isChanged = true;
1407 }
1408 }
1409
1410 if (isChanged) {
1411 // Signal change.
1412 QModelIndex modelIndex = createIndex(clipIndex, 0, trackIndex);
1413 QVector<int> roles;
1414 roles << FadeInRole;
1415 emit dataChanged(modelIndex, modelIndex, roles);
1416 emit modified();
1417 }
1418 }
1419 }
1420 }
1421
fadeOut(int trackIndex,int clipIndex,int duration)1422 void MultitrackModel::fadeOut(int trackIndex, int clipIndex, int duration)
1423 {
1424 int i = m_trackList.at(trackIndex).mlt_index;
1425 QScopedPointer<Mlt::Producer> track(m_tractor->track(i));
1426 if (track) {
1427 Mlt::Playlist playlist(*track);
1428 QScopedPointer<Mlt::ClipInfo> info(playlist.clip_info(clipIndex));
1429 if (info && info->producer && info->producer->is_valid()) {
1430 QScopedPointer<Mlt::Filter> filter;
1431 duration = qBound(0, duration, info->frame_count);
1432 bool isChanged = false;
1433
1434 if (m_trackList[trackIndex].type == VideoTrackType
1435 // If video track index is not None.
1436 && (!info->producer->get("video_index") || info->producer->get_int("video_index") != -1)) {
1437
1438 // Get video filter.
1439 if (Settings.playerGPU())
1440 filter.reset(getFilter("fadeOutMovit", info->producer));
1441 else
1442 filter.reset(getFilter("fadeOutBrightness", info->producer));
1443
1444 if (duration > 0) {
1445 // Add video filter if needed.
1446 if (!filter) {
1447 if (Settings.playerGPU()) {
1448 Mlt::Filter f(MLT.profile(), "movit.opacity");
1449 f.set(kShotcutFilterProperty, "fadeOutMovit");
1450 f.set("alpha", 1);
1451 info->producer->attach(f);
1452 filter.reset(new Mlt::Filter(f));
1453 } else {
1454 Mlt::Filter f(MLT.profile(), "brightness");
1455 f.set(kShotcutFilterProperty, "fadeOutBrightness");
1456 f.set("alpha", 1);
1457 info->producer->attach(f);
1458 filter.reset(new Mlt::Filter(f));
1459 }
1460 filter->set_in_and_out(info->frame_in, info->frame_out);
1461 emit filterOutChanged(info->frame_out, filter.data());
1462 }
1463
1464 // Adjust video filter.
1465 if (Settings.playerGPU()) {
1466 // Special handling for animation keyframes on movit.opacity.
1467 filter->clear("opacity");
1468 filter->anim_set("opacity", 1, info->frame_count - duration, 0, mlt_keyframe_smooth);
1469 filter->anim_set("opacity", 0, info->frame_count - 1);
1470 } else {
1471 // Special handling for animation keyframes on brightness.
1472 const char* key = filter->get_int("alpha") != 1? "alpha" : "level";
1473 filter->clear(key);
1474 filter->anim_set(key, 1, info->frame_count - duration);
1475 filter->anim_set(key, 0, info->frame_count - 1);
1476 }
1477 filter->set(kShotcutAnimOutProperty, duration);
1478 isChanged = true;
1479 } else if (filter) {
1480 // Remove the video filter.
1481 info->producer->detach(*filter);
1482 emit filterAddedOrRemoved(info->producer);
1483 filter->set(kShotcutAnimOutProperty, duration);
1484 isChanged = true;
1485 }
1486 }
1487
1488 // If audio track index is not None.
1489 if (!info->producer->get("audio_index") || info->producer->get_int("audio_index") != -1) {
1490 // Get audio filter.
1491 filter.reset(getFilter("fadeOutVolume", info->producer));
1492
1493 if (duration > 0) {
1494 // Add audio filter if needed.
1495 if (!filter) {
1496 Mlt::Filter f(MLT.profile(), "volume");
1497 f.set(kShotcutFilterProperty, "fadeOutVolume");
1498 info->producer->attach(f);
1499 filter.reset(new Mlt::Filter(f));
1500 filter->set_in_and_out(info->frame_in, info->frame_out);
1501 emit filterOutChanged(info->frame_out, filter.data());
1502 }
1503
1504 // Adjust audio filter.
1505 filter->clear("level");
1506 filter->anim_set("level", 0, info->frame_count - duration);
1507 filter->anim_set("level", -60, info->frame_count - 1);
1508 filter->set(kShotcutAnimOutProperty, duration);
1509 isChanged = true;
1510 } else if (filter) {
1511 // Remove the audio filter.
1512 info->producer->detach(*filter);
1513 emit filterAddedOrRemoved(info->producer);
1514 filter->set(kShotcutAnimOutProperty, duration);
1515 isChanged = true;
1516 }
1517 }
1518
1519 if (isChanged) {
1520 // Signal change.
1521 QModelIndex modelIndex = createIndex(clipIndex, 0, trackIndex);
1522 QVector<int> roles;
1523 roles << FadeOutRole;
1524 emit dataChanged(modelIndex, modelIndex, roles);
1525 emit modified();
1526 }
1527 }
1528 }
1529 }
1530
addTransitionValid(int fromTrack,int toTrack,int clipIndex,int position,bool ripple)1531 bool MultitrackModel::addTransitionValid(int fromTrack, int toTrack, int clipIndex, int position, bool ripple)
1532 {
1533 bool result = false;
1534 int i = m_trackList.at(toTrack).mlt_index;
1535 QScopedPointer<Mlt::Producer> track(m_tractor->track(i));
1536 if (track) {
1537 Mlt::Playlist playlist(*track);
1538 if (fromTrack == toTrack) {
1539 int targetIndex = playlist.get_clip_index_at(position);
1540 int previousIndex = clipIndex - 1 - (playlist.is_blank(clipIndex - 1)? 1 : 0);
1541 int nextIndex = clipIndex + 1 + (playlist.is_blank(clipIndex + 1)? 1 : 0);
1542 int endOfPreviousClip = playlist.clip_start(previousIndex) + playlist.clip_length(previousIndex) - 1;
1543 int endOfCurrentClip = position + playlist.clip_length(clipIndex) - 1;
1544 int startOfNextClip = playlist.clip_start(nextIndex);
1545 auto isBlankAtPosition = playlist.is_blank_at(position);
1546 auto isTransitionAtPreviousIndex = isTransition(playlist, previousIndex);
1547 auto isBlankAtEndOfCurrentClip = playlist.is_blank_at(endOfCurrentClip);
1548 auto isTransitionAtNextIndex = isTransition(playlist, nextIndex);
1549
1550 if ((targetIndex < clipIndex && (endOfCurrentClip > endOfPreviousClip) && (position > playlist.clip_start(previousIndex)) && !isBlankAtPosition && !isTransitionAtPreviousIndex)
1551 ||
1552 (!ripple && (targetIndex >= clipIndex) && (position < startOfNextClip) && !isBlankAtEndOfCurrentClip && !isTransitionAtNextIndex)) {
1553 result = true;
1554 }
1555 }
1556 }
1557 return result;
1558 }
1559
addTransition(int trackIndex,int clipIndex,int position,bool ripple,bool rippleAllTracks)1560 int MultitrackModel::addTransition(int trackIndex, int clipIndex, int position, bool ripple, bool rippleAllTracks)
1561 {
1562 int i = m_trackList.at(trackIndex).mlt_index;
1563 QScopedPointer<Mlt::Producer> track(m_tractor->track(i));
1564 if (track) {
1565 Mlt::Playlist playlist(*track);
1566 int targetIndex = playlist.get_clip_index_at(position);
1567 int previousIndex = clipIndex - 1 - (playlist.is_blank(clipIndex - 1)? 1 : 0);
1568 int nextIndex = clipIndex + 1 + (playlist.is_blank(clipIndex + 1)? 1 : 0);
1569 int endOfPreviousClip = playlist.clip_start(previousIndex) + playlist.clip_length(previousIndex) - 1;
1570 int endOfCurrentClip = position + playlist.clip_length(clipIndex) - 1;
1571 int startOfNextClip = playlist.clip_start(nextIndex);
1572
1573 if ((targetIndex < clipIndex && endOfCurrentClip > endOfPreviousClip) || // dragged left
1574 (targetIndex >= clipIndex && position < startOfNextClip)) { // dragged right
1575 int duration = qAbs(position - playlist.clip_start(clipIndex));
1576
1577 // Remove a blank duration from the transition duration.
1578 if (playlist.is_blank(clipIndex - 1) && targetIndex < clipIndex)
1579 duration -= playlist.clip_length(clipIndex - 1);
1580 else if (playlist.is_blank(clipIndex + 1) && targetIndex >= clipIndex)
1581 duration -= playlist.clip_length(clipIndex + 1);
1582
1583 // Adjust/insert blanks
1584 moveClipInBlank(playlist, trackIndex, clipIndex, position, ripple, rippleAllTracks, duration);
1585 targetIndex = playlist.get_clip_index_at(position);
1586
1587 // Create mix
1588 beginInsertRows(index(trackIndex), targetIndex + 1, targetIndex + 1);
1589 playlist.mix(targetIndex, duration);
1590 Mlt::Producer producer(playlist.get_clip(targetIndex + 1));
1591 if (producer.is_valid()) {
1592 producer.parent().set(kShotcutTransitionProperty, kShotcutDefaultTransition);
1593 }
1594 endInsertRows();
1595
1596 // Add transitions
1597 Mlt::Transition dissolve(MLT.profile(), Settings.playerGPU()? "movit.luma_mix" : "luma");
1598 Mlt::Transition crossFade(MLT.profile(), "mix:-1");
1599 if (!Settings.playerGPU()) dissolve.set("alpha_over", 1);
1600 playlist.mix_add(targetIndex + 1, &dissolve);
1601 playlist.mix_add(targetIndex + 1, &crossFade);
1602
1603 // Notify ins and outs changed
1604 QModelIndex modelIndex = createIndex(targetIndex, 0, trackIndex);
1605 QVector<int> roles;
1606 roles << StartRole;
1607 roles << OutPointRole;
1608 roles << DurationRole;
1609 emit dataChanged(modelIndex, modelIndex, roles);
1610 modelIndex = createIndex(targetIndex + 2, 0, trackIndex);
1611 roles.clear();
1612 roles << StartRole;
1613 roles << InPointRole;
1614 roles << DurationRole;
1615 roles << AudioLevelsRole;
1616 emit dataChanged(modelIndex, modelIndex, roles);
1617 emit modified();
1618 return targetIndex + 1;
1619 }
1620 }
1621 return -1;
1622 }
1623
clearMixReferences(int trackIndex,int clipIndex)1624 void MultitrackModel::clearMixReferences(int trackIndex, int clipIndex)
1625 {
1626 int i = m_trackList.at(trackIndex).mlt_index;
1627 QScopedPointer<Mlt::Producer> track(m_tractor->track(i));
1628 if (track) {
1629 Mlt::Playlist playlist(*track);
1630 QScopedPointer<Mlt::Producer> producer(playlist.get_clip(clipIndex - 1));
1631 if (producer && producer->is_valid()) {
1632 // Clear these since they are no longer valid.
1633 producer->set("mix_in", NULL, 0);
1634 producer->set("mix_out", NULL, 0);
1635 producer.reset(playlist.get_clip(clipIndex));
1636 if (producer && producer->is_valid()) {
1637 producer->parent().set("mlt_mix", NULL, 0);
1638 producer->set("mix_in", NULL, 0);
1639 producer->set("mix_out", NULL, 0);
1640 }
1641 producer.reset(playlist.get_clip(clipIndex + 1));
1642 if (producer && producer->is_valid()) {
1643 producer->set("mix_in", NULL, 0);
1644 producer->set("mix_out", NULL, 0);
1645 }
1646 }
1647 }
1648 }
1649
removeTransition(int trackIndex,int clipIndex)1650 void MultitrackModel::removeTransition(int trackIndex, int clipIndex)
1651 {
1652 int i = m_trackList.at(trackIndex).mlt_index;
1653 QScopedPointer<Mlt::Producer> track(m_tractor->track(i));
1654 if (track) {
1655 Mlt::Playlist playlist(*track);
1656 clearMixReferences(trackIndex, clipIndex);
1657 beginRemoveRows(index(trackIndex), clipIndex, clipIndex);
1658 playlist.remove(clipIndex);
1659 endRemoveRows();
1660 --clipIndex;
1661
1662 QModelIndex modelIndex = createIndex(clipIndex, 0, trackIndex);
1663 QVector<int> roles;
1664 roles << OutPointRole;
1665 roles << DurationRole;
1666 emit dataChanged(modelIndex, modelIndex, roles);
1667 modelIndex = createIndex(clipIndex + 1, 0, trackIndex);
1668 roles << InPointRole;
1669 roles << DurationRole;
1670 emit dataChanged(modelIndex, modelIndex, roles);
1671 emit modified();
1672 }
1673 }
1674
removeTransitionByTrimIn(int trackIndex,int clipIndex,int delta)1675 void MultitrackModel::removeTransitionByTrimIn(int trackIndex, int clipIndex, int delta)
1676 {
1677 QModelIndex modelIndex = index(clipIndex, 0, index(trackIndex));
1678 clearMixReferences(trackIndex, clipIndex);
1679 int duration = -data(modelIndex, MultitrackModel::DurationRole).toInt();
1680 liftClip(trackIndex, clipIndex);
1681 trimClipOut(trackIndex, clipIndex - 1, duration, false, false);
1682 notifyClipOut(trackIndex, clipIndex - 1);
1683 if (delta) {
1684 trimClipIn(trackIndex, clipIndex, delta, false, false);
1685 notifyClipIn(trackIndex, clipIndex);
1686 }
1687 }
1688
removeTransitionByTrimOut(int trackIndex,int clipIndex,int delta)1689 void MultitrackModel::removeTransitionByTrimOut(int trackIndex, int clipIndex, int delta)
1690 {
1691 QModelIndex modelIndex = index(clipIndex + 1, 0, index(trackIndex));
1692 clearMixReferences(trackIndex, clipIndex);
1693 int duration = -data(modelIndex, MultitrackModel::DurationRole).toInt();
1694 liftClip(trackIndex, clipIndex + 1);
1695 trimClipIn(trackIndex, clipIndex + 2, duration, false, false);
1696 notifyClipIn(trackIndex, clipIndex + 1);
1697 if (delta) {
1698 trimClipOut(trackIndex, clipIndex, delta, false, false);
1699 notifyClipOut(trackIndex, clipIndex);
1700 }
1701 }
1702
trimTransitionInValid(int trackIndex,int clipIndex,int delta)1703 bool MultitrackModel::trimTransitionInValid(int trackIndex, int clipIndex, int delta)
1704 {
1705 if (m_isMakingTransition) return false;
1706 bool result = false;
1707 int i = m_trackList.at(trackIndex).mlt_index;
1708 QScopedPointer<Mlt::Producer> track(m_tractor->track(i));
1709 if (track) {
1710 Mlt::Playlist playlist(*track);
1711 if (clipIndex + 2 < playlist.count()) {
1712 Mlt::ClipInfo info;
1713 // Check if there is already a transition and its new length valid.
1714 if (isTransition(playlist, clipIndex + 1) && playlist.clip_length(clipIndex + 1) + delta > 0) {
1715 // Check clip A out point.
1716 playlist.clip_info(clipIndex, &info);
1717 info.frame_out -= delta;
1718 if (info.frame_out > info.frame_in && info.frame_out < info.length) {
1719 // Check clip B in point.
1720 playlist.clip_info(clipIndex + 2, &info);
1721 info.frame_in -= playlist.clip_length(clipIndex + 1) + delta;
1722 if (info.frame_in >= 0 && info.frame_in <= info.frame_out)
1723 result = true;
1724 }
1725 }
1726 }
1727 }
1728 return result;
1729 }
1730
trimTransitionIn(int trackIndex,int clipIndex,int delta)1731 void MultitrackModel::trimTransitionIn(int trackIndex, int clipIndex, int delta)
1732 {
1733 // LOG_DEBUG() << "clipIndex" << clipIndex << "delta" << delta;
1734 int i = m_trackList.at(trackIndex).mlt_index;
1735 QScopedPointer<Mlt::Producer> track(m_tractor->track(i));
1736 if (track) {
1737 Mlt::Playlist playlist(*track);
1738
1739 // Adjust the playlist "mix" entry.
1740 QScopedPointer<Mlt::Producer> producer(playlist.get_clip(clipIndex + 1));
1741 Mlt::Tractor tractor(producer->parent());
1742 if (!tractor.is_valid())
1743 return;
1744 QScopedPointer<Mlt::Producer> track_a(tractor.track(0));
1745 QScopedPointer<Mlt::Producer> track_b(tractor.track(1));
1746 int out = playlist.clip_length(clipIndex + 1) + delta - 1;
1747 playlist.block();
1748 track_a->set_in_and_out(track_a->get_in() - delta, track_a->get_out());
1749 track_b->set_in_and_out(track_b->get_in() - delta, track_b->get_out());
1750 playlist.unblock();
1751 tractor.multitrack()->set_in_and_out(0, out);
1752 tractor.set_in_and_out(0, out);
1753 producer->set("length", producer->frames_to_time(out + 1, mlt_time_clock));
1754 producer->set_in_and_out(0, out);
1755
1756 // Adjust the transitions.
1757 QScopedPointer<Mlt::Service> service(tractor.producer());
1758 while (service && service->is_valid()) {
1759 if (service->type() == transition_type) {
1760 Mlt::Transition transition(*service);
1761 transition.set_in_and_out(0, out);
1762 }
1763 service.reset(service->producer());
1764 }
1765
1766 // Adjust clip entry being trimmed.
1767 Mlt::ClipInfo info;
1768 playlist.clip_info(clipIndex, &info);
1769 playlist.resize_clip(clipIndex, info.frame_in, info.frame_out - delta);
1770
1771 // Adjust filters.
1772 playlist.clip_info(clipIndex + 2, &info);
1773 adjustClipFilters(*info.producer, info.frame_in, info.frame_out, -(out + 1), 0);
1774
1775 QVector<int> roles;
1776 roles << OutPointRole;
1777 roles << DurationRole;
1778 emit dataChanged(createIndex(clipIndex, 0, trackIndex),
1779 createIndex(clipIndex + 1, 0, trackIndex), roles);
1780 emit modified();
1781 }
1782 }
1783
trimTransitionOutValid(int trackIndex,int clipIndex,int delta)1784 bool MultitrackModel::trimTransitionOutValid(int trackIndex, int clipIndex, int delta)
1785 {
1786 if (m_isMakingTransition) return false;
1787 bool result = false;
1788 int i = m_trackList.at(trackIndex).mlt_index;
1789 QScopedPointer<Mlt::Producer> track(m_tractor->track(i));
1790 if (track) {
1791 Mlt::Playlist playlist(*track);
1792 if (clipIndex > 1) {
1793 Mlt::ClipInfo info;
1794 // Check if there is already a transition.
1795 if (isTransition(playlist, clipIndex - 1)) {
1796 // Check clip A out point.
1797 playlist.clip_info(clipIndex - 2, &info);
1798 info.frame_out += playlist.clip_length(clipIndex - 1) + delta;
1799 if (info.frame_out > info.frame_in && info.frame_out < info.length) {
1800 // Check clip B in point.
1801 playlist.clip_info(clipIndex, &info);
1802 info.frame_in += delta;
1803 if (info.frame_in >= 0 && info.frame_in <= info.frame_out)
1804 result = true;
1805 }
1806 }
1807 }
1808 }
1809 return result;
1810 }
1811
trimTransitionOut(int trackIndex,int clipIndex,int delta)1812 void MultitrackModel::trimTransitionOut(int trackIndex, int clipIndex, int delta)
1813 {
1814 // LOG_DEBUG() << "clipIndex" << clipIndex << "delta" << delta;
1815 int i = m_trackList.at(trackIndex).mlt_index;
1816 QScopedPointer<Mlt::Producer> track(m_tractor->track(i));
1817 if (track) {
1818 Mlt::Playlist playlist(*track);
1819
1820 // Adjust the playlist "mix" entry.
1821 QScopedPointer<Mlt::Producer> producer(playlist.get_clip(clipIndex - 1));
1822 Mlt::Tractor tractor(producer->parent());
1823 if (!tractor.is_valid())
1824 return;
1825 QScopedPointer<Mlt::Producer> track_a(tractor.track(0));
1826 QScopedPointer<Mlt::Producer> track_b(tractor.track(1));
1827 int out = playlist.clip_length(clipIndex - 1) + delta - 1;
1828 playlist.block();
1829 track_a->set_in_and_out(track_a->get_in(), track_a->get_out() + delta);
1830 track_b->set_in_and_out(track_b->get_in(), track_b->get_out() + delta);
1831 playlist.unblock();
1832 tractor.multitrack()->set_in_and_out(0, out);
1833 tractor.set_in_and_out(0, out);
1834 producer->set("length", producer->frames_to_time(out + 1, mlt_time_clock));
1835 producer->set_in_and_out(0, out);
1836
1837 // Adjust the transitions.
1838 QScopedPointer<Mlt::Service> service(tractor.producer());
1839 while (service && service->is_valid()) {
1840 if (service->type() == transition_type) {
1841 Mlt::Transition transition(*service);
1842 transition.set_in_and_out(0, out);
1843 }
1844 service.reset(service->producer());
1845 }
1846
1847 // Adjust clip entry being trimmed.
1848 Mlt::ClipInfo info;
1849 playlist.clip_info(clipIndex, &info);
1850 playlist.resize_clip(clipIndex, info.frame_in + delta, info.frame_out);
1851
1852 // Adjust filters.
1853 playlist.clip_info(clipIndex - 2, &info);
1854 adjustClipFilters(*info.producer, info.frame_in, info.frame_out, 0, -(out + 1));
1855
1856 QVector<int> roles;
1857 roles << OutPointRole;
1858 roles << DurationRole;
1859 emit dataChanged(createIndex(clipIndex - 1, 0, trackIndex),
1860 createIndex(clipIndex - 1, 0, trackIndex), roles);
1861 roles.clear();
1862 roles << InPointRole;
1863 roles << DurationRole;
1864 emit dataChanged(createIndex(clipIndex, 0, trackIndex),
1865 createIndex(clipIndex, 0, trackIndex), roles);
1866 emit modified();
1867 }
1868 }
1869
addTransitionByTrimInValid(int trackIndex,int clipIndex,int delta)1870 bool MultitrackModel::addTransitionByTrimInValid(int trackIndex, int clipIndex, int delta)
1871 {
1872 Q_UNUSED(delta)
1873 bool result = false;
1874 int i = m_trackList.at(trackIndex).mlt_index;
1875 QScopedPointer<Mlt::Producer> track(m_tractor->track(i));
1876 if (track) {
1877 Mlt::Playlist playlist(*track);
1878 if (clipIndex > 0) {
1879 // Check if preceding clip is not blank, not already a transition,
1880 // and there is enough frames before in point of current clip.
1881 if (!m_isMakingTransition && delta < 0 && !playlist.is_blank(clipIndex - 1) && !isTransition(playlist, clipIndex - 1)) {
1882 Mlt::ClipInfo info;
1883 playlist.clip_info(clipIndex, &info);
1884 if (info.frame_in >= -delta)
1885 result = true;
1886 } else if (m_isMakingTransition && isTransition(playlist, clipIndex - 1)) {
1887 // Invalid if transition length will be 0 or less.
1888 auto newTransitionDuration = playlist.clip_length(clipIndex - 1) - delta;
1889 result = newTransitionDuration > 0;
1890 if (result && clipIndex > 1) {
1891 QScopedPointer<Mlt::ClipInfo> info(playlist.clip_info(clipIndex));
1892 // Invalid if left clip length will be 0 or less.
1893 result = playlist.clip_length(clipIndex - 2) + delta > 0 &&
1894 // Invalid if current clip in point will be less than 0.
1895 info && info->frame_in - newTransitionDuration >= 0;
1896 }
1897 } else {
1898 result = m_isMakingTransition;
1899 }
1900 }
1901 }
1902 return result;
1903 }
1904
addTransitionByTrimIn(int trackIndex,int clipIndex,int delta)1905 int MultitrackModel::addTransitionByTrimIn(int trackIndex, int clipIndex, int delta)
1906 {
1907 int i = m_trackList.at(trackIndex).mlt_index;
1908 QScopedPointer<Mlt::Producer> track(m_tractor->track(i));
1909 if (track) {
1910 Mlt::Playlist playlist(*track);
1911
1912 // Create transition if it does not yet exist.
1913 if (!isTransition(playlist, clipIndex - 1)) {
1914 // Adjust filters.
1915 Mlt::ClipInfo info;
1916 playlist.clip_info(clipIndex, &info);
1917 adjustClipFilters(*info.producer, info.frame_in, info.frame_out, delta, 0);
1918
1919 // Insert the mix clip.
1920 beginInsertRows(index(trackIndex), clipIndex, clipIndex);
1921 playlist.mix_out(clipIndex - 1, -delta);
1922 QScopedPointer<Mlt::Producer> producer(playlist.get_clip(clipIndex));
1923 producer->parent().set(kShotcutTransitionProperty, kShotcutDefaultTransition);
1924 endInsertRows();
1925
1926 // Add transitions.
1927 Mlt::Transition dissolve(MLT.profile(), Settings.playerGPU()? "movit.luma_mix" : "luma");
1928 Mlt::Transition crossFade(MLT.profile(), "mix:-1");
1929 if (!Settings.playerGPU()) dissolve.set("alpha_over", 1);
1930 playlist.mix_add(clipIndex, &dissolve);
1931 playlist.mix_add(clipIndex, &crossFade);
1932
1933 // Notify clip A changed.
1934 QModelIndex modelIndex = createIndex(clipIndex - 1, 0, trackIndex);
1935 QVector<int> roles;
1936 roles << OutPointRole;
1937 roles << DurationRole;
1938 emit dataChanged(modelIndex, modelIndex, roles);
1939 emit modified();
1940 m_isMakingTransition = true;
1941 clipIndex += 1;
1942 } else if (m_isMakingTransition) {
1943 // Adjust a transition addition already in progress.
1944 // m_isMakingTransition will be set false when mouse button released via notifyClipOut().
1945 trimTransitionIn(trackIndex, clipIndex - 2, -delta);
1946 }
1947 }
1948 return clipIndex;
1949 }
1950
addTransitionByTrimOutValid(int trackIndex,int clipIndex,int delta)1951 bool MultitrackModel::addTransitionByTrimOutValid(int trackIndex, int clipIndex, int delta)
1952 {
1953 Q_UNUSED(delta)
1954 bool result = false;
1955 int i = m_trackList.at(trackIndex).mlt_index;
1956 QScopedPointer<Mlt::Producer> track(m_tractor->track(i));
1957 if (track) {
1958 Mlt::Playlist playlist(*track);
1959 if (clipIndex + 1 < playlist.count()) {
1960 // Check if following clip is not blank, not already a transition,
1961 // and there is enough frames after out point of current clip.
1962 if (!m_isMakingTransition && delta < 0 && !playlist.is_blank(clipIndex + 1) && !isTransition(playlist, clipIndex + 1)) {
1963 Mlt::ClipInfo info;
1964 playlist.clip_info(clipIndex, &info);
1965 // LOG_DEBUG() << "(info.length" << info.length << " - info.frame_out" << info.frame_out << ") =" << (info.length - info.frame_out) << " >= -delta" << -delta;
1966 if ((info.length - info.frame_out) >= -delta)
1967 result = true;
1968 } else if (m_isMakingTransition && isTransition(playlist, clipIndex + 1)) {
1969 // Invalid if transition length will be 0 or less.
1970 auto newTransitionDuration = playlist.clip_length(clipIndex + 1) - delta;
1971 // LOG_DEBUG() << "playlist.clip_length(clipIndex + 1)" << playlist.clip_length(clipIndex + 1) << "- delta" << delta << "=" << (playlist.clip_length(clipIndex + 1) - delta);
1972 result = newTransitionDuration > 0;
1973 if (result && clipIndex + 2 < playlist.count()) {
1974 QScopedPointer<Mlt::ClipInfo> info(playlist.clip_info(clipIndex));
1975 // Invalid if right clip length will be 0 or less.
1976 result = playlist.clip_length(clipIndex + 2) + delta > 0 &&
1977 // Invalid if current clip out point would exceed its duration.
1978 info && info->frame_out + newTransitionDuration < info->length;
1979 }
1980 } else {
1981 result = m_isMakingTransition;
1982 }
1983 }
1984 }
1985 return result;
1986 }
1987
addTransitionByTrimOut(int trackIndex,int clipIndex,int delta)1988 void MultitrackModel::addTransitionByTrimOut(int trackIndex, int clipIndex, int delta)
1989 {
1990 int i = m_trackList.at(trackIndex).mlt_index;
1991 QScopedPointer<Mlt::Producer> track(m_tractor->track(i));
1992 if (track) {
1993 Mlt::Playlist playlist(*track);
1994
1995 // Create transition if it does not yet exist.
1996 if (!isTransition(playlist, clipIndex + 1)) {
1997 // Adjust filters.
1998 Mlt::ClipInfo info;
1999 playlist.clip_info(clipIndex, &info);
2000 adjustClipFilters(*info.producer, info.frame_in, info.frame_out, 0, delta);
2001
2002 // Insert the mix clip.
2003 beginInsertRows(index(trackIndex), clipIndex + 1, clipIndex + 1);
2004 playlist.mix_in(clipIndex, -delta);
2005 QScopedPointer<Mlt::Producer> producer(playlist.get_clip(clipIndex + 1));
2006 producer->parent().set(kShotcutTransitionProperty, kShotcutDefaultTransition);
2007 endInsertRows();
2008
2009 // Add transitions.
2010 Mlt::Transition dissolve(MLT.profile(), Settings.playerGPU()? "movit.luma_mix" : "luma");
2011 Mlt::Transition crossFade(MLT.profile(), "mix:-1");
2012 if (!Settings.playerGPU()) dissolve.set("alpha_over", 1);
2013 playlist.mix_add(clipIndex + 1, &dissolve);
2014 playlist.mix_add(clipIndex + 1, &crossFade);
2015
2016 // Notify clip B changed.
2017 QModelIndex modelIndex = createIndex(clipIndex + 2, 0, trackIndex);
2018 QVector<int> roles;
2019 roles << InPointRole;
2020 roles << DurationRole;
2021 emit dataChanged(modelIndex, modelIndex, roles);
2022 emit modified();
2023 m_isMakingTransition = true;
2024 } else if (m_isMakingTransition) {
2025 // Adjust a transition addition already in progress.
2026 // m_isMakingTransition will be set false when mouse button released via notifyClipIn().
2027 delta = playlist.clip_start(clipIndex + 1) - (playlist.clip_start(clipIndex) + playlist.clip_length(clipIndex) + delta);
2028 trimTransitionOut(trackIndex, clipIndex + 2, delta);
2029 }
2030 }
2031 }
2032
removeTransitionByTrimInValid(int trackIndex,int clipIndex,int delta)2033 bool MultitrackModel::removeTransitionByTrimInValid(int trackIndex, int clipIndex, int delta)
2034 {
2035 bool result = false;
2036 int i = m_trackList.at(trackIndex).mlt_index;
2037 QScopedPointer<Mlt::Producer> track(m_tractor->track(i));
2038 if (track) {
2039 Mlt::Playlist playlist(*track);
2040 if (clipIndex > 1) {
2041 // Check if there is a transition and its new length is 0 or less.
2042 if (isTransition(playlist, clipIndex - 1) && playlist.clip_length(clipIndex - 1) - qAbs(delta) <= 0) {
2043 result = true;
2044 m_isMakingTransition = false;
2045 }
2046 }
2047 }
2048 return result;
2049 }
2050
removeTransitionByTrimOutValid(int trackIndex,int clipIndex,int delta)2051 bool MultitrackModel::removeTransitionByTrimOutValid(int trackIndex, int clipIndex, int delta)
2052 {
2053 bool result = false;
2054 int i = m_trackList.at(trackIndex).mlt_index;
2055 QScopedPointer<Mlt::Producer> track(m_tractor->track(i));
2056 if (track) {
2057 Mlt::Playlist playlist(*track);
2058 if (clipIndex + 2 < playlist.count()) {
2059 // Check if there is a transition and its new length is 0 or less.
2060 // LOG_DEBUG() << "transition length" << playlist.clip_length(clipIndex + 1) << "delta" << delta << playlist.clip_length(clipIndex + 1) - qAbs(delta);
2061 if (isTransition(playlist, clipIndex + 1) && playlist.clip_length(clipIndex + 1) - qAbs(delta) <= 0) {
2062 result = true;
2063 m_isMakingTransition = false;
2064 }
2065 }
2066 }
2067 return result;
2068 }
2069
filterAddedOrRemoved(Mlt::Producer * producer)2070 void MultitrackModel::filterAddedOrRemoved(Mlt::Producer* producer)
2071 {
2072 if (!m_tractor || !producer || !producer->is_valid())
2073 return;
2074 mlt_service service = producer->get_service();
2075
2076 // Check if it was on the multitrack tractor.
2077 if (service == m_tractor->get_service())
2078 emit filteredChanged();
2079 else if (producer->get(kMultitrackItemProperty)) {
2080 // Check if it was a clip.
2081 QString s = QString::fromLatin1(producer->get(kMultitrackItemProperty));
2082 QVector<QStringRef> parts = s.splitRef(':');
2083 if (parts.length() == 2) {
2084 QModelIndex modelIndex = createIndex(parts[0].toInt(), 0, parts[1].toInt());
2085 QVector<int> roles;
2086 roles << FadeInRole;
2087 roles << FadeOutRole;
2088 emit dataChanged(modelIndex, modelIndex, roles);
2089 }
2090 } else for (int i = 0; i < m_trackList.size(); i++) {
2091 // Check if it was on one of the tracks.
2092 QScopedPointer<Mlt::Producer> track(m_tractor->track(m_trackList[i].mlt_index));
2093 if (service == track.data()->get_service()) {
2094 QModelIndex modelIndex = index(i, 0);
2095 QVector<int> roles;
2096 roles << IsFilteredRole;
2097 emit dataChanged(modelIndex, modelIndex, roles);
2098 break;
2099 }
2100 }
2101 }
2102
onFilterChanged(Mlt::Filter * filter)2103 void MultitrackModel::onFilterChanged(Mlt::Filter* filter)
2104 {
2105 if (filter && filter->is_valid()) {
2106 Mlt::Service service(mlt_service(filter->get_data("service")));
2107 if (service.is_valid() && service.get(kMultitrackItemProperty)) {
2108 QString s = QString::fromLatin1(service.get(kMultitrackItemProperty));
2109 QVector<QStringRef> parts = s.splitRef(':');
2110 if (parts.length() == 2) {
2111 QModelIndex modelIndex = createIndex(parts[0].toInt(), 0, parts[1].toInt());
2112 QVector<int> roles;
2113 const char* name = filter->get(kShotcutFilterProperty);
2114 if (!qstrcmp("fadeInMovit", name) ||
2115 !qstrcmp("fadeInBrightness", name) ||
2116 !qstrcmp("fadeInVolume", name))
2117 roles << FadeInRole;
2118 if (!qstrcmp("fadeOutMovit", name) ||
2119 !qstrcmp("fadeOutBrightness", name) ||
2120 !qstrcmp("fadeOutVolume", name))
2121 roles << FadeOutRole;
2122 if (roles.length())
2123 emit dataChanged(modelIndex, modelIndex, roles);
2124 }
2125 }
2126 }
2127 }
2128
moveClipToEnd(Mlt::Playlist & playlist,int trackIndex,int clipIndex,int position,bool ripple,bool rippleAllTracks)2129 void MultitrackModel::moveClipToEnd(Mlt::Playlist& playlist, int trackIndex, int clipIndex, int position, bool ripple, bool rippleAllTracks)
2130 {
2131 int n = playlist.count();
2132 int length = position - playlist.clip_start(n - 1) - playlist.clip_length(n - 1);
2133 int clipPlaytime = playlist.clip_length(clipIndex);
2134 int clipStart = playlist.clip_start(clipIndex);
2135
2136 if (!ripple) {
2137 if (clipIndex > 0 && playlist.is_blank(clipIndex - 1)) {
2138 // If there was a blank on the left adjust it.
2139 int duration = playlist.clip_length(clipIndex - 1) + playlist.clip_length(clipIndex);
2140 // LOG_DEBUG() << "adjust blank on left to" << duration;
2141 playlist.resize_clip(clipIndex - 1, 0, duration - 1);
2142
2143 QModelIndex index = createIndex(clipIndex - 1, 0, trackIndex);
2144 QVector<int> roles;
2145 roles << DurationRole;
2146 emit dataChanged(index, index, roles);
2147 } else if ((clipIndex + 1) < n && playlist.is_blank(clipIndex + 1)) {
2148 // If there was a blank on the right adjust it.
2149 int duration = playlist.clip_length(clipIndex + 1) + playlist.clip_length(clipIndex);
2150 // LOG_DEBUG() << "adjust blank on right to" << duration;
2151 playlist.resize_clip(clipIndex + 1, 0, duration - 1);
2152
2153 QModelIndex index = createIndex(clipIndex + 1, 0, trackIndex);
2154 QVector<int> roles;
2155 roles << DurationRole;
2156 emit dataChanged(index, index, roles);
2157 } else {
2158 // Add new blank
2159 beginInsertRows(index(trackIndex), clipIndex, clipIndex);
2160 playlist.insert_blank(clipIndex, playlist.clip_length(clipIndex) - 1);
2161 endInsertRows();
2162 ++clipIndex;
2163 ++n;
2164 }
2165 }
2166 // Add blank to end if needed.
2167 if (length > 0) {
2168 beginInsertRows(index(trackIndex), n, n);
2169 playlist.blank(length - 1);
2170 endInsertRows();
2171 }
2172 // Finally, move clip into place.
2173 QModelIndex parentIndex = index(trackIndex);
2174 if (playlist.count() < clipIndex || playlist.count() > clipIndex + 1) {
2175 beginMoveRows(parentIndex, clipIndex, clipIndex, parentIndex, playlist.count());
2176 playlist.move(clipIndex, playlist.count());
2177 endMoveRows();
2178 consolidateBlanks(playlist, trackIndex);
2179 }
2180
2181 // Ripple all unlocked tracks.
2182 if (clipPlaytime > 0 && ripple && rippleAllTracks)
2183 for (int j = 0; j < m_trackList.count(); ++j) {
2184 if (j == trackIndex)
2185 continue;
2186
2187 int mltIndex = m_trackList.at(j).mlt_index;
2188 QScopedPointer<Mlt::Producer> otherTrack(m_tractor->track(mltIndex));
2189 if (otherTrack) {
2190 if (otherTrack->get_int(kTrackLockProperty))
2191 continue;
2192
2193 removeRegion(j, clipStart, clipPlaytime);
2194 }
2195 }
2196 }
2197
moveClipInBlank(Mlt::Playlist & playlist,int trackIndex,int clipIndex,int position,bool ripple,bool rippleAllTracks,int duration)2198 void MultitrackModel::moveClipInBlank(Mlt::Playlist& playlist, int trackIndex, int clipIndex, int position, bool ripple, bool rippleAllTracks, int duration)
2199 {
2200 int clipPlaytime = duration? duration : playlist.clip_length(clipIndex);
2201 int clipStart = playlist.clip_start(clipIndex);
2202 int delta = position - clipStart;
2203
2204 if (clipIndex > 0 && playlist.is_blank(clipIndex - 1)) {
2205 // Adjust blank on left.
2206 int duration = playlist.clip_length(clipIndex - 1) + delta;
2207 if (duration > 0) {
2208 // LOG_DEBUG() << "adjust blank on left" << (clipIndex - 1) << "to" << duration;
2209 playlist.resize_clip(clipIndex - 1, 0, duration - 1);
2210
2211 QModelIndex index = createIndex(clipIndex - 1, 0, trackIndex);
2212 QVector<int> roles;
2213 roles << DurationRole;
2214 emit dataChanged(index, index, roles);
2215 } else {
2216 // LOG_DEBUG() << "remove blank on left";
2217 int i = clipIndex - 1;
2218 beginRemoveRows(index(trackIndex), i, i);
2219 playlist.remove(i);
2220 endRemoveRows();
2221 consolidateBlanks(playlist, trackIndex);
2222 --clipIndex;
2223 }
2224 } else if (delta > 0) {
2225 // LOG_DEBUG() << "add blank on left with duration" << delta;
2226 // Add blank to left.
2227 int i = qMax(clipIndex, 0);
2228 beginInsertRows(index(trackIndex), i, i);
2229 playlist.insert_blank(i, delta - 1);
2230 endInsertRows();
2231 ++clipIndex;
2232 }
2233
2234 if (!ripple && (clipIndex + 1) < playlist.count() && playlist.is_blank(clipIndex + 1)) {
2235 // Adjust blank to right.
2236 int duration = playlist.clip_length(clipIndex + 1) - delta;
2237 if (duration > 0) {
2238 // LOG_DEBUG() << "adjust blank on right" << (clipIndex + 1) << "to" << duration;
2239 playlist.resize_clip(clipIndex + 1, 0, duration - 1);
2240
2241 QModelIndex index = createIndex(clipIndex + 1, 0, trackIndex);
2242 QVector<int> roles;
2243 roles << DurationRole;
2244 emit dataChanged(index, index, roles);
2245 } else {
2246 // LOG_DEBUG() << "remove blank on right";
2247 int i = clipIndex + 1;
2248 beginRemoveRows(index(trackIndex), i, i);
2249 playlist.remove(i);
2250 endRemoveRows();
2251 consolidateBlanks(playlist, trackIndex);
2252 }
2253 } else if (!ripple && delta < 0 && (clipIndex + 1) < playlist.count()) {
2254 // Add blank to right.
2255 // LOG_DEBUG() << "add blank on right with duration" << -delta;
2256 beginInsertRows(index(trackIndex), clipIndex + 1, clipIndex + 1);
2257 playlist.insert_blank(clipIndex + 1, (-delta - 1));
2258 endInsertRows();
2259 }
2260
2261 // Ripple all unlocked tracks.
2262 if (clipPlaytime > 0 && ripple && rippleAllTracks) {
2263 QList<int> otherTracksToRipple;
2264 for (int i = 0; i < m_trackList.count(); ++i) {
2265 if (i == trackIndex)
2266 continue;
2267 int mltIndex = m_trackList.at(i).mlt_index;
2268 QScopedPointer<Mlt::Producer> otherTrack(m_tractor->track(mltIndex));
2269 if (otherTrack && otherTrack->get_int(kTrackLockProperty))
2270 continue;
2271 otherTracksToRipple << i;
2272 }
2273 if (position < clipStart) {
2274 foreach (int idx, otherTracksToRipple)
2275 removeRegion(idx, position, clipStart - position);
2276 } else {
2277 insertOrAdjustBlankAt(otherTracksToRipple, clipStart, position - clipStart);
2278 consolidateBlanks(playlist, trackIndex);
2279 }
2280 }
2281 }
2282
consolidateBlanks(Mlt::Playlist & playlist,int trackIndex)2283 void MultitrackModel::consolidateBlanks(Mlt::Playlist &playlist, int trackIndex)
2284 {
2285 for (int i = 1; i < playlist.count(); i++) {
2286 if (playlist.is_blank(i - 1) && playlist.is_blank(i)) {
2287 int out = playlist.clip_length(i - 1) + playlist.clip_length(i) - 1;
2288 playlist.resize_clip(i - 1, 0, out);
2289 QModelIndex idx = createIndex(i - 1, 0, trackIndex);
2290 QVector<int> roles;
2291 roles << DurationRole;
2292 emit dataChanged(idx, idx, roles);
2293 beginRemoveRows(index(trackIndex), i, i);
2294 playlist.remove(i--);
2295 endRemoveRows();
2296 }
2297 }
2298 if (playlist.count() > 0) {
2299 int i = playlist.count() - 1;
2300 if (playlist.is_blank(i)) {
2301 beginRemoveRows(index(trackIndex), i, i);
2302 playlist.remove(i);
2303 endRemoveRows();
2304 }
2305 }
2306 if (playlist.count() == 0) {
2307 beginInsertRows(index(trackIndex), 0, 0);
2308 playlist.blank(0);
2309 endInsertRows();
2310 }
2311 }
2312
consolidateBlanksAllTracks()2313 void MultitrackModel::consolidateBlanksAllTracks()
2314 {
2315 if (!m_tractor) return;
2316 int i = 0;
2317 foreach (Track t, m_trackList) {
2318 Mlt::Producer* track = m_tractor->track(t.mlt_index);
2319 if (track) {
2320 Mlt::Playlist playlist(*track);
2321 consolidateBlanks(playlist, i);
2322 }
2323 ++i;
2324 }
2325 }
2326
audioLevelsReady(const QModelIndex & index)2327 void MultitrackModel::audioLevelsReady(const QModelIndex& index)
2328 {
2329 QVector<int> roles;
2330 roles << AudioLevelsRole;
2331 emit dataChanged(index, index, roles);
2332 }
2333
createIfNeeded()2334 bool MultitrackModel::createIfNeeded()
2335 {
2336 if (!m_tractor) {
2337 m_tractor = new Mlt::Tractor(MLT.profile());
2338 MLT.profile().set_explicit(true);
2339 m_tractor->set(kShotcutXmlProperty, 1);
2340 retainPlaylist();
2341 addBackgroundTrack();
2342 addVideoTrack();
2343 emit created();
2344 } else if (!m_trackList.count()) {
2345 addVideoTrack();
2346 }
2347 return true;
2348 }
2349
addBackgroundTrack()2350 void MultitrackModel::addBackgroundTrack()
2351 {
2352 Mlt::Playlist playlist(MLT.profile());
2353 playlist.set("id", kBackgroundTrackId);
2354 Mlt::Producer producer(MLT.profile(), "color:0");
2355 producer.set("mlt_image_format", "rgb24a");
2356 producer.set("length", 1);
2357 producer.set("id", "black");
2358 // Allow mixing against frames produced by this producer.
2359 producer.set("set.test_audio", 0);
2360 playlist.append(producer);
2361 m_tractor->set_track(playlist, m_tractor->count());
2362 }
2363
adjustBackgroundDuration()2364 void MultitrackModel::adjustBackgroundDuration()
2365 {
2366 if (!m_tractor) return;
2367 int duration = getDuration();
2368 Mlt::Producer* track = m_tractor->track(0);
2369 if (track) {
2370 Mlt::Playlist playlist(*track);
2371 Mlt::Producer* clip = playlist.get_clip(0);
2372 if (clip) {
2373 if (duration != clip->parent().get_length()) {
2374 clip->parent().set("length", clip->parent().frames_to_time(duration, mlt_time_clock));
2375 clip->parent().set_in_and_out(0, duration - 1);
2376 clip->set("length", clip->parent().frames_to_time(duration, mlt_time_clock));
2377 clip->set_in_and_out(0, duration - 1);
2378 playlist.resize_clip(0, 0, duration - 1);
2379 emit durationChanged();
2380 }
2381 delete clip;
2382 }
2383 delete track;
2384 }
2385 }
2386
adjustServiceFilterDurations(Mlt::Service & service,int duration)2387 void MultitrackModel::adjustServiceFilterDurations(Mlt::Service& service, int duration)
2388 {
2389 // Use kFilterOutProperty to track duration changes
2390 if (service.get(kFilterOutProperty)) {
2391 int oldOut = service.get_int(kFilterOutProperty);
2392 int n = service.filter_count();
2393 for (int i = 0; i < n; i++) {
2394 QScopedPointer<Mlt::Filter> filter(service.filter(i));
2395 if (filter && filter->is_valid() && !filter->get_int("_loader")) {
2396 int in = filter->get_in();
2397 int out = filter->get_out();
2398 // Only change out if it is pinned (same as old track duration)
2399 if (out == oldOut || in < 0) {
2400 out = duration - 1;
2401 filter->set_in_and_out(qMax(in, 0), out);
2402 }
2403 }
2404 }
2405 }
2406 service.set(kFilterOutProperty, duration - 1);
2407 }
2408
warnIfInvalid(Mlt::Service & service)2409 bool MultitrackModel::warnIfInvalid(Mlt::Service& service)
2410 {
2411 if (!service.is_valid()) {
2412 const char* plugin = Settings.playerGPU()? "Movit overlay" : "frei0r cairoblend";
2413 const char* plugins = Settings.playerGPU()? "Movit" : "frei0r";
2414 LongUiTask::cancel();
2415 QMessageBox::critical(&MAIN, qApp->applicationName(),
2416 tr("Error: Shotcut could not find the %1 plugin on your system.\n\n"
2417 "Please install the %2 plugins.").arg(plugin).arg(plugins));
2418 return true;
2419 }
2420 return false;
2421 }
2422
adjustTrackFilters()2423 void MultitrackModel::adjustTrackFilters()
2424 {
2425 if (!m_tractor) return;
2426 int duration = getDuration();
2427
2428 // Adjust filters on the tractor.
2429 adjustServiceFilterDurations(*m_tractor, duration);
2430
2431 // Adjust filters on the tracks.
2432 foreach (Track t, m_trackList) {
2433 QScopedPointer<Mlt::Producer> track(m_tractor->track(t.mlt_index));
2434 if (track && track->is_valid())
2435 adjustServiceFilterDurations(*track, duration);
2436 }
2437 }
2438
adjustClipFilters(Mlt::Producer & producer,int in,int out,int inDelta,int outDelta)2439 void MultitrackModel::adjustClipFilters(Mlt::Producer& producer, int in, int out, int inDelta, int outDelta)
2440 {
2441 for (int j = 0; j < producer.filter_count(); j++) {
2442 QScopedPointer<Mlt::Filter> filter(producer.filter(j));
2443 if (filter && filter->is_valid()) {
2444 QString filterName = filter->get(kShotcutFilterProperty);
2445
2446 if (inDelta) {
2447 if (filterName.startsWith("fadeIn")) {
2448 if (!filter->get(kShotcutAnimInProperty)) {
2449 // Convert legacy fadeIn filters.
2450 filter->set(kShotcutAnimInProperty, filter->get_length());
2451 }
2452 filter->set_in_and_out(in + inDelta, filter->get_out());
2453 emit filterInChanged(inDelta, filter.data());
2454 } else if (!filter->get_int("_loader") && filter->get_in() <= in) {
2455 filter->set_in_and_out(in + inDelta, filter->get_out());
2456 emit filterInChanged(inDelta, filter.data());
2457 }
2458 }
2459
2460 if (filterName.startsWith("fadeOut")) {
2461 if (!filter->get(kShotcutAnimOutProperty)) {
2462 // Convert legacy fadeOut filters.
2463 filter->set(kShotcutAnimOutProperty, filter->get_length());
2464 }
2465 filter->set_in_and_out(filter->get_in(), out - outDelta);
2466 if (filterName == "fadeOutBrightness") {
2467 const char* key = filter->get_int("alpha") != 1? "alpha" : "level";
2468 filter->clear(key);
2469 filter->anim_set(key, 1, filter->get_length() - filter->get_int(kShotcutAnimOutProperty));
2470 filter->anim_set(key, 0, filter->get_length() - 1);
2471 } else if (filterName == "fadeOutMovit") {
2472 filter->clear("opacity");
2473 filter->anim_set("opacity", 1, filter->get_length() - filter->get_int(kShotcutAnimOutProperty), 0, mlt_keyframe_smooth);
2474 filter->anim_set("opacity", 0, filter->get_length() - 1);
2475 } else if (filterName == "fadeOutVolume") {
2476 filter->clear("level");
2477 filter->anim_set("level", 0, filter->get_length() - filter->get_int(kShotcutAnimOutProperty));
2478 filter->anim_set("level", -60, filter->get_length() - 1);
2479 }
2480 emit filterOutChanged(outDelta, filter.data());
2481 } else if (!filter->get_int("_loader") && filter->get_out() >= out) {
2482 filter->set_in_and_out(filter->get_in(), out - outDelta);
2483 emit filterOutChanged(outDelta, filter.data());
2484
2485 // Update simple keyframes of non-current filters.
2486 if (filter->get_int(kShotcutAnimOutProperty) > 0
2487 && MAIN.filterController()->currentFilter()
2488 && MAIN.filterController()->currentFilter()->filter().get_filter() != filter.data()->get_filter()) {
2489 QmlMetadata* meta = MAIN.filterController()->metadataForService(filter.data());
2490 if (meta && meta->keyframes()) {
2491 foreach (QString name, meta->keyframes()->simpleProperties()) {
2492 if (!filter->get_animation(name.toUtf8().constData()))
2493 // Cause a string property to be interpreted as animated value.
2494 filter->anim_get_double(name.toUtf8().constData(), 0, filter->get_length());
2495 Mlt::Animation animation = filter->get_animation(name.toUtf8().constData());
2496 if (animation.is_valid()) {
2497 int n = animation.key_count();
2498 if (n > 1) {
2499 animation.set_length(filter->get_length());
2500 animation.key_set_frame(n - 2, filter->get_length() - filter->get_int(kShotcutAnimOutProperty));
2501 animation.key_set_frame(n - 1, filter->get_length() - 1);
2502 }
2503 }
2504 }
2505 }
2506 }
2507 }
2508 }
2509 }
2510 }
2511
findClipByUuid(const QUuid & uuid,int & trackIndex,int & clipIndex)2512 Mlt::ClipInfo* MultitrackModel::findClipByUuid(const QUuid &uuid, int &trackIndex, int &clipIndex)
2513 {
2514 for (trackIndex = 0; trackIndex < trackList().size(); trackIndex++) {
2515 int i = trackList().at(trackIndex).mlt_index;
2516 QScopedPointer<Mlt::Producer> track(tractor()->track(i));
2517 if (track) {
2518 Mlt::Playlist playlist(*track);
2519 for (clipIndex = 0; clipIndex < playlist.count(); clipIndex++) {
2520 Mlt::ClipInfo* info;
2521 if ((info = playlist.clip_info(clipIndex))) {
2522 if (MLT.uuid(*info->cut) == uuid)
2523 return info;
2524 }
2525 }
2526 }
2527 }
2528 return nullptr;
2529 }
2530
addAudioTrack()2531 int MultitrackModel::addAudioTrack()
2532 {
2533 if (!m_tractor) {
2534 m_tractor = new Mlt::Tractor(MLT.profile());
2535 MLT.profile().set_explicit(true);
2536 m_tractor->set(kShotcutXmlProperty, 1);
2537 retainPlaylist();
2538 addBackgroundTrack();
2539 addAudioTrack();
2540 emit created();
2541 emit modified();
2542 return 0;
2543 }
2544
2545 // Get the new track index.
2546 int i = m_tractor->count();
2547
2548 // Create the MLT track.
2549 Mlt::Playlist playlist(MLT.profile());
2550 playlist.set(kAudioTrackProperty, 1);
2551 playlist.set("hide", 1);
2552 playlist.blank(0);
2553 m_tractor->set_track(playlist, i);
2554 MLT.updateAvformatCaching(m_tractor->count());
2555
2556 // Add the mix transition.
2557 Mlt::Transition mix(MLT.profile(), "mix");
2558 mix.set("always_active", 1);
2559 mix.set("sum", 1);
2560 m_tractor->plant_transition(mix, 0, i);
2561
2562 // Get the new, logical audio-only index.
2563 int a = 0;
2564 foreach (Track t, m_trackList) {
2565 if (t.type == AudioTrackType)
2566 ++a;
2567 }
2568
2569 // Add the shotcut logical audio track.
2570 Track t;
2571 t.mlt_index = i;
2572 t.type = AudioTrackType;
2573 t.number = a++;
2574 QString trackName = QString("A%1").arg(a);
2575 playlist.set(kTrackNameProperty, trackName.toUtf8().constData());
2576 beginInsertRows(QModelIndex(), m_trackList.count(), m_trackList.count());
2577 m_trackList.append(t);
2578 endInsertRows();
2579 emit modified();
2580 return m_trackList.count() - 1;
2581 }
2582
addVideoTrack()2583 int MultitrackModel::addVideoTrack()
2584 {
2585 if (!m_tractor) {
2586 createIfNeeded();
2587 return 0;
2588 }
2589
2590 // Get the new track index.
2591 int i = m_tractor->count();
2592
2593 // Create the MLT track.
2594 Mlt::Playlist playlist(MLT.profile());
2595 playlist.set(kVideoTrackProperty, 1);
2596 playlist.blank(0);
2597 m_tractor->set_track(playlist, i);
2598 MLT.updateAvformatCaching(m_tractor->count());
2599
2600 // Add the mix transition.
2601 Mlt::Transition mix(MLT.profile(), "mix");
2602 mix.set("always_active", 1);
2603 mix.set("sum", 1);
2604 m_tractor->plant_transition(mix, 0, i);
2605
2606 // Add the composite transition.
2607 Mlt::Transition composite(MLT.profile(), Settings.playerGPU()? "movit.overlay" : "frei0r.cairoblend");
2608 if (!composite.is_valid() && !Settings.playerGPU()) {
2609 composite = Mlt::Transition(MLT.profile(), "qtblend");
2610 } else if (composite.is_valid() && !Settings.playerGPU()) {
2611 composite.set("threads", 0);
2612 }
2613 if (warnIfInvalid(composite)) {
2614 return -1;
2615 }
2616 composite.set("disable", 1);
2617 foreach (Track t, m_trackList) {
2618 if (t.type == VideoTrackType) {
2619 composite.set("disable", 0);
2620 break;
2621 }
2622 }
2623
2624 // Get the new, logical video-only index.
2625 int v = 0;
2626 int last_mlt_index = 0;
2627 foreach (Track t, m_trackList) {
2628 if (t.type == VideoTrackType) {
2629 ++v;
2630 last_mlt_index = t.mlt_index;
2631 }
2632 }
2633 m_tractor->plant_transition(composite, last_mlt_index, i);
2634
2635 // Add the shotcut logical video track.
2636 Track t;
2637 t.mlt_index = i;
2638 t.type = VideoTrackType;
2639 t.number = v++;
2640 QString trackName = QString("V%1").arg(v);
2641 playlist.set(kTrackNameProperty, trackName.toUtf8().constData());
2642 beginInsertRows(QModelIndex(), 0, 0);
2643 m_trackList.prepend(t);
2644 endInsertRows();
2645 emit modified();
2646 return 0;
2647 }
2648
removeTrack(int trackIndex)2649 void MultitrackModel::removeTrack(int trackIndex)
2650 {
2651 if (trackIndex >= 0 && trackIndex < m_trackList.size()) {
2652 const Track& track = m_trackList.value(trackIndex);
2653 QScopedPointer<Mlt::Transition> transition(getTransition("frei0r.cairoblend", track.mlt_index));
2654
2655 // Remove transitions.
2656 if (!transition)
2657 transition.reset(getTransition("qtblend", track.mlt_index));
2658 if (!transition)
2659 transition.reset(getTransition("movit.overlay", track.mlt_index));
2660 if (transition)
2661 m_tractor->field()->disconnect_service(*transition);
2662 transition.reset(getTransition("mix", track.mlt_index));
2663 if (transition)
2664 m_tractor->field()->disconnect_service(*transition);
2665
2666 // foreach (Track t, m_trackList) LOG_DEBUG() << (t.type == VideoTrackType?"Video":"Audio") << "track number" << t.number << "mlt_index" << t.mlt_index;
2667 // LOG_DEBUG() << trackIndex << "mlt_index" << track.mlt_index;
2668
2669 // Remove track.
2670 beginRemoveRows(QModelIndex(), trackIndex, trackIndex);
2671 m_tractor->remove_track(track.mlt_index);
2672 m_trackList.removeAt(trackIndex);
2673 endRemoveRows();
2674 MLT.updateAvformatCaching(m_tractor->count());
2675
2676 // foreach (Track t, m_trackList) LOG_DEBUG() << (t.type == VideoTrackType?"Video":"Audio") << "track number" << t.number << "mlt_index" << t.mlt_index;
2677
2678 // Renumber other tracks.
2679 int row = 0;
2680 foreach (Track t, m_trackList) {
2681 if (t.mlt_index > track.mlt_index)
2682 --m_trackList[row].mlt_index;
2683 if (t.type == track.type && t.number > track.number) {
2684 --m_trackList[row].number;
2685 QModelIndex modelIndex = index(row, 0);
2686
2687 // Disable compositing on the bottom video track.
2688 if (m_trackList[row].number == 0 && t.type == VideoTrackType) {
2689 QScopedPointer<Mlt::Transition> transition(getTransition("frei0r.cairoblend", 1));
2690 if (!transition)
2691 transition.reset(getTransition("qtblend", 1));
2692 if (!transition)
2693 transition.reset(getTransition("movit.overlay", 1));
2694 if (transition && transition->is_valid())
2695 transition->set("disable", 1);
2696 emit dataChanged(modelIndex, modelIndex, QVector<int>() << IsBottomVideoRole << IsCompositeRole);
2697 }
2698
2699 // Rename default track names.
2700 QScopedPointer<Mlt::Producer> mltTrack(m_tractor->track(m_trackList[row].mlt_index));
2701 QString trackNameTemplate = (t.type == VideoTrackType)? QString("V%1") : QString("A%1");
2702 QString trackName = trackNameTemplate.arg(t.number + 1);
2703 if (mltTrack && mltTrack->get(kTrackNameProperty) == trackName) {
2704 trackName = trackNameTemplate.arg(m_trackList[row].number + 1);
2705 mltTrack->set(kTrackNameProperty, trackName.toUtf8().constData());
2706 emit dataChanged(modelIndex, modelIndex, QVector<int>() << NameRole);
2707 }
2708 }
2709 ++row;
2710 }
2711 // foreach (Track t, m_trackList) LOG_DEBUG() << (t.type == VideoTrackType?"Video":"Audio") << "track number" << t.number << "mlt_index" << t.mlt_index;
2712 }
2713 emit modified();
2714 }
2715
retainPlaylist()2716 void MultitrackModel::retainPlaylist()
2717 {
2718 if (!MAIN.playlist())
2719 MAIN.playlistDock()->model()->createIfNeeded();
2720 Mlt::Playlist playlist(*MAIN.playlist());
2721 playlist.set("id", kPlaylistTrackId);
2722 QString retain = QString("xml_retain %1").arg(kPlaylistTrackId);
2723 m_tractor->set(retain.toUtf8().constData(), playlist.get_service(), 0);
2724 }
2725
loadPlaylist()2726 void MultitrackModel::loadPlaylist()
2727 {
2728 Mlt::Properties retainList((mlt_properties) m_tractor->get_data("xml_retain"));
2729 if (retainList.is_valid()) {
2730 Mlt::Playlist playlist((mlt_playlist) retainList.get_data(kPlaylistTrackId));
2731 if (playlist.is_valid() && playlist.type() == playlist_type) {
2732 MAIN.playlistDock()->model()->setPlaylist(playlist);
2733 } else {
2734 playlist = (mlt_playlist) retainList.get_data(kLegacyPlaylistTrackId);
2735 if (playlist.is_valid() && playlist.type() == playlist_type)
2736 MAIN.playlistDock()->model()->setPlaylist(playlist);
2737 }
2738 }
2739 retainPlaylist();
2740 }
2741
removeRegion(int trackIndex,int position,int length)2742 void MultitrackModel::removeRegion(int trackIndex, int position, int length)
2743 {
2744 int i = m_trackList.at(trackIndex).mlt_index;
2745 QScopedPointer<Mlt::Producer> track(m_tractor->track(i));
2746 if (track) {
2747 Mlt::Playlist playlist(*track);
2748 int clipIndex = playlist.get_clip_index_at(position);
2749
2750 if (clipIndex >= 0 && clipIndex < playlist.count())
2751 {
2752 int clipStart = playlist.clip_start(clipIndex);
2753 int playtime = playlist.get_playtime();
2754 playlist.block(playlist.get_playlist());
2755
2756 if (position + length > playtime)
2757 length -= (position + length - playtime);
2758
2759 if (clipStart < position) {
2760 splitClip(trackIndex, clipIndex, position);
2761 ++clipIndex;
2762 }
2763
2764 while (length > 0) {
2765 if (playlist.clip_length(clipIndex) > length) {
2766 splitClip(trackIndex, clipIndex, position + length);
2767 }
2768 length -= playlist.clip_length(clipIndex);
2769 if (clipIndex < playlist.count()) {
2770 // Shotcut does not like the behavior of remove() on a
2771 // transition (MLT mix clip). So, we null mlt_mix to prevent it.
2772 clearMixReferences(trackIndex, clipIndex);
2773 beginRemoveRows(index(trackIndex), clipIndex, clipIndex);
2774 playlist.remove(clipIndex);
2775 endRemoveRows();
2776 }
2777 }
2778 playlist.unblock(playlist.get_playlist());
2779 consolidateBlanks(playlist, trackIndex);
2780 }
2781 }
2782 }
2783
isTransition(Mlt::Playlist & playlist,int clipIndex) const2784 bool MultitrackModel::isTransition(Mlt::Playlist &playlist, int clipIndex) const
2785 {
2786 QScopedPointer<Mlt::Producer> producer(playlist.get_clip(clipIndex));
2787 if (producer && producer->parent().get(kShotcutTransitionProperty))
2788 return true;
2789 return false;
2790 }
2791
insertTrack(int trackIndex,TrackType type)2792 void MultitrackModel::insertTrack(int trackIndex, TrackType type)
2793 {
2794 if (!m_tractor || trackIndex <= 0) {
2795 addVideoTrack();
2796 return;
2797 }
2798
2799 // Get the new track index.
2800 Track& track = m_trackList[qBound(0, trackIndex, m_trackList.count() - 1)];
2801 int i = track.mlt_index;
2802 QScopedPointer<Mlt::Transition> lower;
2803 const char* videoTransitionName = Settings.playerGPU()? "movit.overlay" : "frei0r.cairoblend";
2804 if (type == VideoTrackType) {
2805 ++i;
2806 auto transition = getTransition(videoTransitionName, track.mlt_index);
2807 if (!transition) {
2808 transition = getTransition("qtblend", track.mlt_index);
2809 }
2810 lower.reset(transition);
2811 }
2812
2813 if (trackIndex >= m_trackList.count()) {
2814 if (type == AudioTrackType) {
2815 addAudioTrack();
2816 return;
2817 } else if (type == VideoTrackType) {
2818 i = track.mlt_index;
2819 }
2820 }
2821
2822 // foreach (Track t, m_trackList) LOG_DEBUG() << (t.type == VideoTrackType?"Video":"Audio") << "track number" << t.number << "mlt_index" << t.mlt_index;
2823 // LOG_DEBUG() << "trackIndex" << trackIndex << "mlt_index" << i;
2824
2825 // Get the new, logical video-only index.
2826 int videoTrackCount = 0;
2827 int last_mlt_index = 0;
2828 int row = 0;
2829 foreach (Track t, m_trackList) {
2830 if (t.type == track.type) {
2831 if ((t.type == VideoTrackType && t.number > track.number) ||
2832 (t.type == AudioTrackType && t.number >= track.number)) {
2833 // Rename default track names.
2834 QScopedPointer<Mlt::Producer> mltTrack(m_tractor->track(t.mlt_index));
2835 QString trackNameTemplate = (t.type == VideoTrackType)? QString("V%1") : QString("A%1");
2836 QString trackName = trackNameTemplate.arg(++t.number);
2837 if (mltTrack && mltTrack->get(kTrackNameProperty) == trackName) {
2838 trackName = trackNameTemplate.arg(t.number + 1);
2839 mltTrack->set(kTrackNameProperty, trackName.toUtf8().constData());
2840 QModelIndex modelIndex = index(row, 0);
2841 QVector<int> roles;
2842 roles << NameRole;
2843 emit dataChanged(modelIndex, modelIndex, roles);
2844 }
2845 ++m_trackList[row].number;
2846 }
2847 }
2848 if (t.mlt_index >= i)
2849 ++m_trackList[row].mlt_index;
2850 if (t.type == VideoTrackType) {
2851 ++videoTrackCount;
2852 last_mlt_index = t.mlt_index;
2853 }
2854 ++row;
2855 }
2856
2857 // foreach (Track t, m_trackList) LOG_DEBUG() << (t.type == VideoTrackType?"Video":"Audio") << "track number" << t.number << "mlt_index" << t.mlt_index;
2858
2859 // Create the MLT track.
2860 Mlt::Playlist playlist(MLT.profile());
2861 if (type == VideoTrackType) {
2862 playlist.set(kVideoTrackProperty, 1);
2863 } else if (type == AudioTrackType) {
2864 playlist.set(kAudioTrackProperty, 1);
2865 playlist.set("hide", 1);
2866 }
2867 playlist.blank(0);
2868 m_tractor->insert_track(playlist, i);
2869 MLT.updateAvformatCaching(m_tractor->count());
2870
2871 // Add the mix transition.
2872 Mlt::Transition mix(MLT.profile(), "mix");
2873 mix.set("always_active", 1);
2874 mix.set("sum", 1);
2875 m_tractor->plant_transition(mix, 0, i);
2876
2877 if (type == VideoTrackType) {
2878 // Add the composite transition.
2879 Mlt::Transition composite(MLT.profile(), videoTransitionName);
2880 if (!composite.is_valid() && !Settings.playerGPU()) {
2881 composite = Mlt::Transition(MLT.profile(), "qtblend");
2882 } else if (composite.is_valid() && !Settings.playerGPU()) {
2883 composite.set("threads", 0);
2884 }
2885 if (warnIfInvalid(composite)) {
2886 return;
2887 }
2888 if (lower) {
2889 QScopedPointer<Mlt::Service> consumer(lower->consumer());
2890 if (consumer->is_valid()) {
2891 // Insert the new transition.
2892 LOG_DEBUG() << "inserting transition" << last_mlt_index << i;
2893 composite.connect(*lower, last_mlt_index, i);
2894 Mlt::Transition t((mlt_transition) consumer->get_service());
2895 t.connect(composite, consumer->get_int("a_track"), consumer->get_int("b_track"));
2896 } else {
2897 // Append the new transition.
2898 LOG_DEBUG() << "appending transition";
2899 m_tractor->plant_transition(composite, last_mlt_index, i);
2900 }
2901 } else {
2902 // Append the new transition.
2903 LOG_DEBUG() << "appending transition";
2904 m_tractor->plant_transition(composite, last_mlt_index, i);
2905 }
2906 }
2907
2908 // Add the shotcut logical video track.
2909 Track t;
2910 t.mlt_index = i;
2911 t.type = type;
2912 QString trackName;
2913 if (t.type == VideoTrackType) {
2914 t.number = videoTrackCount - trackIndex;
2915 trackName = QString("V%1");
2916 } else if (t.type == AudioTrackType) {
2917 t.number = trackIndex - videoTrackCount;
2918 trackName = QString("A%1");
2919 }
2920 trackName = trackName.arg(t.number + 1);
2921 playlist.set(kTrackNameProperty, trackName.toUtf8().constData());
2922 beginInsertRows(QModelIndex(), trackIndex, trackIndex);
2923 m_trackList.insert(trackIndex, t);
2924 endInsertRows();
2925 emit modified();
2926 // foreach (Track t, m_trackList) LOG_DEBUG() << (t.type == VideoTrackType?"Video":"Audio") << "track number" << t.number << "mlt_index" << t.mlt_index;
2927 }
2928
insertOrAdjustBlankAt(QList<int> tracks,int position,int length)2929 void MultitrackModel::insertOrAdjustBlankAt(QList<int> tracks, int position, int length)
2930 {
2931 foreach (int trackIndex, tracks) {
2932 int mltIndex = m_trackList.at(trackIndex).mlt_index;
2933 QScopedPointer<Mlt::Producer> otherTrack(m_tractor->track(mltIndex));
2934
2935 if (otherTrack) {
2936 Mlt::Playlist trackPlaylist(*otherTrack);
2937 int idx = trackPlaylist.get_clip_index_at(position);
2938
2939 if (trackPlaylist.is_blank(idx)) {
2940 trackPlaylist.resize_clip(idx, 0, trackPlaylist.clip_length(idx) + length - 1);
2941 QModelIndex modelIndex = createIndex(idx, 0, trackIndex);
2942 emit dataChanged(modelIndex, modelIndex, QVector<int>() << DurationRole);
2943 } else if (length > 0) {
2944 int insertBlankAtIdx = idx;
2945 if (trackPlaylist.clip_start(idx) < position) {
2946 splitClip(trackIndex, idx, position);
2947 insertBlankAtIdx = idx + 1;
2948 }
2949 beginInsertRows(index(trackIndex), insertBlankAtIdx, insertBlankAtIdx);
2950 trackPlaylist.insert_blank(insertBlankAtIdx, length - 1);
2951 endInsertRows();
2952 } else {
2953 Q_ASSERT(!"unsupported");
2954 }
2955 }
2956 }
2957 }
2958
mergeClipWithNext(int trackIndex,int clipIndex,bool dryrun)2959 bool MultitrackModel::mergeClipWithNext(int trackIndex, int clipIndex, bool dryrun)
2960 {
2961 int i = m_trackList.at(trackIndex).mlt_index;
2962 QScopedPointer<Mlt::Producer> track(m_tractor->track(i));
2963 if (!track)
2964 return false;
2965 Mlt::Playlist playlist(*track);
2966 if (clipIndex >= playlist.count() + 1)
2967 return false;
2968 Mlt::ClipInfo clip1;
2969 Mlt::ClipInfo clip2;
2970 playlist.clip_info(clipIndex, &clip1);
2971 playlist.clip_info(clipIndex + 1, &clip2);
2972
2973 if (QString::fromUtf8(clip1.resource) != QString::fromUtf8(clip2.resource))
2974 return false;
2975
2976 if (clip1.frame_out + 1 != clip2.frame_in)
2977 return false;
2978
2979 if (dryrun)
2980 return true;
2981
2982 // Consolidate filters
2983 QStringList filters {"fadeInVolume", "fadeOutVolume", "fadeInBrightness", "fadeOutBrightness", "fadeInMovit", "fadeOutMovit"};
2984 for (const auto& s : filters) {
2985 QScopedPointer<Mlt::Filter> filter(getFilter(s, clip1.producer));
2986 if (filter && filter->is_valid()) {
2987 filter.reset(getFilter(s, clip2.producer));
2988 if (filter && filter->is_valid()) {
2989 clip2.producer->detach(*filter);
2990 }
2991 }
2992 }
2993 Mlt::Controller::copyFilters(*clip2.producer, *clip1.producer);
2994 QModelIndex modelIndex = createIndex(clipIndex, 0, trackIndex);
2995 QVector<int> roles;
2996 roles << FadeInRole;
2997 roles << FadeOutRole;
2998 emit dataChanged(modelIndex, modelIndex, roles);
2999
3000 liftClip(trackIndex, clipIndex + 1);
3001 trimClipOut(trackIndex, clipIndex, -clip2.frame_count, false, false);
3002
3003 emit modified();
3004 return true;
3005 }
3006
isFiltered(Mlt::Producer * producer) const3007 bool MultitrackModel::isFiltered(Mlt::Producer* producer) const
3008 {
3009 if (!producer)
3010 producer = m_tractor;
3011 if (producer && producer->is_valid()) {
3012 int count = producer->filter_count();
3013 for (int i = 0; i < count; i++) {
3014 QScopedPointer<Mlt::Filter> filter(producer->filter(i));
3015 if (filter && filter->is_valid() && !filter->get_int("_loader"))
3016 return true;
3017 }
3018 }
3019 return false;
3020 }
3021
getDuration()3022 int MultitrackModel::getDuration()
3023 {
3024 int n = 0;
3025 if (m_tractor) {
3026 foreach (Track t, m_trackList) {
3027 QScopedPointer<Mlt::Producer> track(m_tractor->track(t.mlt_index));
3028 if (track && track->is_valid())
3029 n = qMax(n, track->get_length());
3030 }
3031 }
3032 return n;
3033 }
3034
load()3035 void MultitrackModel::load()
3036 {
3037 if (m_tractor) {
3038 beginResetModel();
3039 delete m_tractor;
3040 m_tractor = 0;
3041 m_trackList.clear();
3042 endResetModel();
3043 }
3044 // In some versions of MLT, the resource property is the XML filename,
3045 // but the Mlt::Tractor(Service&) constructor will fail unless it detects
3046 // the type as tractor, and mlt_service_identify() needs the resource
3047 // property to say "<tractor>" to identify it as playlist type.
3048 MLT.producer()->set("mlt_type", "mlt_producer");
3049 MLT.producer()->set("resource", "<tractor>");
3050 MLT.profile().set_explicit(true);
3051 m_tractor = new Mlt::Tractor(*MLT.producer());
3052 if (!m_tractor->is_valid()) {
3053 delete m_tractor;
3054 m_tractor = 0;
3055 return;
3056 }
3057
3058 loadPlaylist();
3059 addBlackTrackIfNeeded();
3060 MLT.updateAvformatCaching(m_tractor->count());
3061 refreshTrackList();
3062 convertOldDoc();
3063 consolidateBlanksAllTracks();
3064 adjustBackgroundDuration();
3065 adjustTrackFilters();
3066 if (m_trackList.count() > 0) {
3067 beginInsertRows(QModelIndex(), 0, m_trackList.count() - 1);
3068 endInsertRows();
3069 getAudioLevels();
3070 }
3071 emit loaded();
3072 emit filteredChanged();
3073 }
3074
reload(bool asynchronous)3075 void MultitrackModel::reload(bool asynchronous)
3076 {
3077 if (m_tractor) {
3078 if (asynchronous) {
3079 emit reloadRequested();
3080 } else {
3081 beginResetModel();
3082 endResetModel();
3083 getAudioLevels();
3084 emit filteredChanged();
3085 }
3086 }
3087 }
3088
replace(int trackIndex,int clipIndex,Mlt::Producer & clip,bool copyFilters)3089 void MultitrackModel::replace(int trackIndex, int clipIndex, Mlt::Producer& clip, bool copyFilters)
3090 {
3091 int i = m_trackList.at(trackIndex).mlt_index;
3092 Mlt::Producer track(m_tractor->track(i));
3093 if (track.is_valid()) {
3094 // LOG_DEBUG() << __FUNCTION__ << "replace" << position << MLT.XML(&clip);
3095 Mlt::Playlist playlist(track);
3096 int in = clip.get_in();
3097 int out = clip.get_out();
3098 Mlt::Producer oldClip(playlist.get_clip(clipIndex));
3099 Q_ASSERT(oldClip.is_valid());
3100 int clipPlaytime = oldClip.get_playtime();
3101 int transitionIn = oldClip.parent().get(kFilterInProperty)? oldClip.get_in() - oldClip.parent().get_int(kFilterInProperty) : 0;
3102 int transitionOut = oldClip.parent().get(kFilterOutProperty)? oldClip.parent().get_int(kFilterOutProperty) - oldClip.get_out() : 0;
3103
3104 in += transitionIn;
3105 out -= transitionOut;
3106 if (clip.get_in() > 0 || clip.get_out() == clip.get_playtime() - 1)
3107 out = in + clipPlaytime - 1;
3108 else
3109 in = out - clipPlaytime + 1;
3110 clip.set_in_and_out(in, out);
3111 if (copyFilters) {
3112 auto parent = oldClip.parent();
3113 parent.set(kFilterInProperty, oldClip.get_in());
3114 parent.set(kFilterOutProperty, oldClip.get_out());
3115 Mlt::Controller::copyFilters(parent, clip);
3116 Mlt::Controller::adjustFilters(clip);
3117 }
3118 beginRemoveRows(index(trackIndex), clipIndex, clipIndex);
3119 playlist.remove(clipIndex);
3120 endRemoveRows();
3121 beginInsertRows(index(trackIndex), clipIndex, clipIndex);
3122 playlist.insert_blank(clipIndex, clipPlaytime - 1);
3123 endInsertRows();
3124 overwrite(trackIndex, clip, playlist.clip_start(clipIndex), false);
3125
3126 // Handle transition on the left
3127 if (transitionIn && isTransition(playlist, clipIndex - 1)) {
3128 Mlt::Producer producer(playlist.get_clip(clipIndex - 1));
3129 if (producer.is_valid()) {
3130 Mlt::Tractor tractor(MLT_TRACTOR(producer.get_parent()));
3131 Q_ASSERT(tractor.is_valid());
3132 Mlt::Producer track(tractor.track(1));
3133 if (!qstrcmp(track.parent().get(kShotcutHashProperty), oldClip.parent().get(kShotcutHashProperty))) {
3134 Mlt::Producer cut(clip.cut(in - transitionIn, in - 1));
3135 tractor.set_track(cut, 1);
3136 }
3137 }
3138 }
3139 // Handle transition on the right
3140 if (transitionOut && isTransition(playlist, clipIndex + 1)) {
3141 Mlt::Producer producer(playlist.get_clip(clipIndex + 1));
3142 if (producer.is_valid()) {
3143 Mlt::Tractor tractor(MLT_TRACTOR(producer.get_parent()));
3144 Q_ASSERT(tractor.is_valid());
3145 Mlt::Producer track(tractor.track(0));
3146 if (!qstrcmp(track.parent().get(kShotcutHashProperty), oldClip.parent().get(kShotcutHashProperty))) {
3147 Mlt::Producer cut(clip.cut(out + 1, out + transitionOut));
3148 tractor.set_track(cut, 0);
3149 }
3150 }
3151 }
3152 }
3153 }
3154
close()3155 void MultitrackModel::close()
3156 {
3157 if (!m_tractor) return;
3158 if (m_trackList.count() > 0) {
3159 beginRemoveRows(QModelIndex(), 0, m_trackList.count() - 1);
3160 m_trackList.clear();
3161 endRemoveRows();
3162 }
3163 delete m_tractor;
3164 m_tractor = 0;
3165 emit closed();
3166 }
3167
clipIndex(int trackIndex,int position)3168 int MultitrackModel::clipIndex(int trackIndex, int position)
3169 {
3170 int i = m_trackList.at(trackIndex).mlt_index;
3171 QScopedPointer<Mlt::Producer> track(m_tractor->track(i));
3172 if (track) {
3173 Mlt::Playlist playlist(*track);
3174 return playlist.get_clip_index_at(position);
3175 }
3176 return -1; // error
3177 }
3178
refreshTrackList()3179 void MultitrackModel::refreshTrackList()
3180 {
3181 int n = m_tractor->count();
3182 int a = 0;
3183 int v = 0;
3184 bool isKdenlive = false;
3185
3186 // Add video tracks in reverse order.
3187 for (int i = 0; i < n; ++i) {
3188 QScopedPointer<Mlt::Producer> track(m_tractor->track(i));
3189 if (!track)
3190 continue;
3191 QString trackId = track->get("id");
3192 if (trackId == "black_track")
3193 isKdenlive = true;
3194 else if (trackId == kBackgroundTrackId)
3195 continue;
3196 else if (!track->get(kShotcutPlaylistProperty) && !track->get(kAudioTrackProperty)) {
3197 int hide = track->get_int("hide");
3198 // hide: 0 = a/v, 2 = muted video track
3199 if (track->get(kVideoTrackProperty) || hide == 0 || hide == 2) {
3200 Track t;
3201 t.mlt_index = i;
3202 t.type = VideoTrackType;
3203 t.number = v++;
3204 QString trackName = track->get(kTrackNameProperty);
3205 if (trackName.isEmpty())
3206 trackName = QString("V%1").arg(v);
3207 track->set(kTrackNameProperty, trackName.toUtf8().constData());
3208 m_trackList.prepend(t);
3209
3210 // Always disable compositing on V1.
3211 if (v == 1) {
3212 QScopedPointer<Mlt::Transition> transition(getTransition("frei0r.cairoblend", 1));
3213 if (!transition)
3214 transition.reset(getTransition("movit.overlay", 1));
3215 if (transition && transition->is_valid())
3216 transition->set("disable", 1);
3217 }
3218 }
3219 }
3220 }
3221
3222 // Add audio tracks.
3223 for (int i = 0; i < n; ++i) {
3224 QScopedPointer<Mlt::Producer> track(m_tractor->track(i));
3225 if (!track)
3226 continue;
3227 QString trackId = track->get("id");
3228 if (trackId == "black_track")
3229 isKdenlive = true;
3230 else if (isKdenlive && trackId == "playlist1")
3231 // In Kdenlive, playlist1 is a special audio mixdown track.
3232 continue;
3233 else if (trackId == kPlaylistTrackId || trackId == kLegacyPlaylistTrackId)
3234 continue;
3235 else if (!track->get(kShotcutPlaylistProperty) && !track->get(kVideoTrackProperty)) {
3236 int hide = track->get_int("hide");
3237 // hide: 1 = audio only track, 3 = muted audio-only track
3238 if (track->get(kAudioTrackProperty) || hide == 1 || hide == 3) {
3239 Track t;
3240 t.mlt_index = i;
3241 t.type = AudioTrackType;
3242 t.number = a++;
3243 QString trackName = track->get(kTrackNameProperty);
3244 if (trackName.isEmpty())
3245 trackName = QString("A%1").arg(a);
3246 track->set(kTrackNameProperty, trackName.toUtf8().constData());
3247 m_trackList.append(t);
3248 // LOG_DEBUG() << __FUNCTION__ << QString(track->get("id")) << i;
3249 }
3250 }
3251 }
3252 }
3253
getAudioLevels()3254 void MultitrackModel::getAudioLevels()
3255 {
3256 for (int trackIx = 0; trackIx < m_trackList.size(); trackIx++) {
3257 int i = m_trackList.at(trackIx).mlt_index;
3258 QScopedPointer<Mlt::Producer> track(m_tractor->track(i));
3259 Mlt::Playlist playlist(*track);
3260 for (int clipIx = 0; clipIx < playlist.count(); clipIx++) {
3261 QScopedPointer<Mlt::Producer> clip(playlist.get_clip(clipIx));
3262 if (clip && clip->is_valid() && !clip->is_blank() && clip->get_int("audio_index") > -1) {
3263 QModelIndex index = createIndex(clipIx, 0, trackIx);
3264 AudioLevelsTask::start(clip->parent(), this, index);
3265 }
3266 }
3267 }
3268 }
3269
addBlackTrackIfNeeded()3270 void MultitrackModel::addBlackTrackIfNeeded()
3271 {
3272 return;
3273 // Make sure the first track is black silence and add it if needed
3274 bool found = false;
3275 int n = m_tractor->count();
3276 QScopedPointer<Mlt::Producer> track(m_tractor->track(0));
3277 if (track) {
3278 Mlt::Playlist playlist(*track);
3279 QScopedPointer<Mlt::Producer> clip(playlist.get_clip(0));
3280 if (clip && QString(clip->get("id")) == "black")
3281 found = true;
3282 }
3283 if (!found) {
3284 // Move all existing tracks down by 1.
3285 for (int i = n; i > 0; i++) {
3286 Mlt::Producer* producer = m_tractor->track(n - 1);
3287 if (producer)
3288 m_tractor->set_track(*producer, n);
3289 delete producer;
3290 }
3291 Mlt::Producer producer(MLT.profile(), "color:0");
3292 producer.set("mlt_image_format", "rgb24a");
3293 m_tractor->set_track(producer, 0);
3294 }
3295 }
3296
convertOldDoc()3297 void MultitrackModel::convertOldDoc()
3298 {
3299 // Convert composite to frei0r.cairoblend.
3300 int n = m_tractor->count();
3301 for (int i = 1; i < n; ++i) {
3302 QScopedPointer<Mlt::Transition> transition(getTransition("composite", i));
3303 if (transition) {
3304 Mlt::Transition composite(MLT.profile(), "frei0r.cairoblend");
3305 composite.set("disable", transition->get_int("disable"));
3306 m_tractor->field()->disconnect_service(*transition);
3307 m_tractor->plant_transition(composite, transition->get_int("a_track"), i);
3308 }
3309 }
3310
3311 // Remove movit.rect filters.
3312 QScopedPointer<Mlt::Service> service(m_tractor->producer());
3313 while (service && service->is_valid()) {
3314 if (service->type() == filter_type) {
3315 Mlt::Filter f((mlt_filter) service->get_service());
3316 if (QString::fromLatin1(f.get("mlt_service")) == "movit.rect") {
3317 m_tractor->field()->disconnect_service(f);
3318 }
3319 }
3320 service.reset(service->producer());
3321 }
3322
3323 // Change a_track of composite transitions to bottom video track.
3324 int a_track = 0;
3325 foreach (Track t, m_trackList) {
3326 if (t.type == VideoTrackType)
3327 a_track = t.mlt_index;
3328 }
3329 QString name = Settings.playerGPU()? "movit.overlay" : "frei0r.cairoblend";
3330 foreach (Track t, m_trackList) {
3331 if (t.type == VideoTrackType) {
3332 QScopedPointer<Mlt::Transition> transition(getTransition(name, t.mlt_index));
3333 if (transition && transition->get_a_track() != 0)
3334 transition->set("a_track", a_track);
3335 }
3336 }
3337
3338 // Ensure the background track clears the test_audio flag on frames.
3339 QScopedPointer<Mlt::Producer> track(m_tractor->track(0));
3340 if (track) {
3341 Mlt::Playlist playlist(*track);
3342 QScopedPointer<Mlt::ClipInfo> info(playlist.clip_info(0));
3343 if (info && info->producer->is_valid() && QString(info->producer->get("id")) == "black")
3344 info->producer->set("set.test_audio", 0);
3345 }
3346 }
3347
getTransition(const QString & name,int trackIndex) const3348 Mlt::Transition *MultitrackModel::getTransition(const QString &name, int trackIndex) const
3349 {
3350 QScopedPointer<Mlt::Service> service(m_tractor->producer());
3351 while (service && service->is_valid()) {
3352 if (service->type() == transition_type) {
3353 Mlt::Transition t((mlt_transition) service->get_service());
3354 if (name == t.get("mlt_service") && t.get_b_track() == trackIndex)
3355 return new Mlt::Transition(t);
3356 }
3357 service.reset(service->producer());
3358 }
3359 return nullptr;
3360 }
3361
getFilter(const QString & name,int trackIndex) const3362 Mlt::Filter *MultitrackModel::getFilter(const QString &name, int trackIndex) const
3363 {
3364 QScopedPointer<Mlt::Service> service(m_tractor->producer());
3365 while (service && service->is_valid()) {
3366 if (service->type() == filter_type) {
3367 Mlt::Filter f((mlt_filter) service->get_service());
3368 if (name == f.get("mlt_service") && f.get_track() == trackIndex)
3369 return new Mlt::Filter(f);
3370 }
3371 service.reset(service->producer());
3372 }
3373 return 0;
3374 }
3375
getFilter(const QString & name,Mlt::Service * service) const3376 Mlt::Filter *MultitrackModel::getFilter(const QString &name, Mlt::Service* service) const
3377 {
3378 return MLT.getFilter(name, service);
3379 }
3380
removeBlankPlaceholder(Mlt::Playlist & playlist,int trackIndex)3381 void MultitrackModel::removeBlankPlaceholder(Mlt::Playlist& playlist, int trackIndex)
3382 {
3383 if (playlist.count() == 1 && playlist.is_blank(0)) {
3384 beginRemoveRows(index(trackIndex), 0, 0);
3385 playlist.remove(0);
3386 endRemoveRows();
3387 }
3388 }
3389