/* * Copyright (c) 2020 Meltytech, LLC * * This program is free software: you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by * the Free Software Foundation, either version 3 of the License, or * (at your option) any later version. * * This program is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * * You should have received a copy of the GNU General Public License * along with this program. If not, see . */ #include "slideshowgeneratorwidget.h" #include "Logger.h" #include "mltcontroller.h" #include "settings.h" #include "shotcut_mlt_properties.h" #include "widgets/producerpreviewwidget.h" #include #include #include #include #include #include #include #include enum { ASPECT_CONVERSION_PAD_BLACK, ASPECT_CONVERSION_CROP_CENTER, ASPECT_CONVERSION_CROP_PAN, ASPECT_CONVERSION_PAD_BLUR, }; static const int minTransitionFrames = 2; SlideshowGeneratorWidget::SlideshowGeneratorWidget(Mlt::Playlist *clips, QWidget *parent) : QWidget(parent) , m_clips(clips) , m_refreshPreview(false) , m_previewProducer(nullptr) { QGridLayout *grid = new QGridLayout(); setLayout(grid); grid->addWidget(new QLabel(tr("Clip duration")), 0, 0, Qt::AlignRight); m_clipDurationSpinner = new QDoubleSpinBox(); m_clipDurationSpinner->setToolTip(tr("Set the duration of each clip in the slideshow.")); m_clipDurationSpinner->setSuffix(" s"); m_clipDurationSpinner->setDecimals(1); m_clipDurationSpinner->setMinimum(0.2); m_clipDurationSpinner->setMaximum(3600 * 4); m_clipDurationSpinner->setValue(10); connect(m_clipDurationSpinner, SIGNAL(valueChanged(double)), this, SLOT(on_parameterChanged())); grid->addWidget(m_clipDurationSpinner, 0, 1); grid->addWidget(new QLabel(tr("Aspect ratio conversion")), 1, 0, Qt::AlignRight); m_aspectConversionCombo = new QComboBox(); m_aspectConversionCombo->addItem(tr("Pad Black")); m_aspectConversionCombo->addItem(tr("Crop Center")); m_aspectConversionCombo->addItem(tr("Crop and Pan")); { QScopedPointer mltFilters(MLT.repository()->filters()); if (mltFilters && mltFilters->property_exists("pillar_echo")) { m_aspectConversionCombo->addItem(tr("Pad Blur")); } } m_aspectConversionCombo->setToolTip(tr("Choose an aspect ratio conversion method.")); m_aspectConversionCombo->setCurrentIndex(ASPECT_CONVERSION_CROP_CENTER); connect(m_aspectConversionCombo, SIGNAL(currentIndexChanged(int)), this, SLOT(on_parameterChanged())); grid->addWidget(m_aspectConversionCombo, 1, 1); grid->addWidget(new QLabel(tr("Zoom effect")), 2, 0, Qt::AlignRight); m_zoomPercentSpinner = new QSpinBox(); m_zoomPercentSpinner->setToolTip( tr("Set the percentage of the zoom-in effect.\n0% will result in no zoom effect.")); m_zoomPercentSpinner->setSuffix(" %"); m_zoomPercentSpinner->setMinimum(-50); m_zoomPercentSpinner->setMaximum(50); m_zoomPercentSpinner->setValue(10); connect(m_zoomPercentSpinner, SIGNAL(valueChanged(int)), this, SLOT(on_parameterChanged())); grid->addWidget(m_zoomPercentSpinner, 2, 1); grid->addWidget(new QLabel(tr("Transition duration")), 3, 0, Qt::AlignRight); m_transitionDurationSpinner = new QDoubleSpinBox(); m_transitionDurationSpinner->setToolTip( tr("Set the duration of the transition.\nMay not be longer than half the duration of the clip.\nIf the duration is 0, no transition will be created.")); m_transitionDurationSpinner->setSuffix(" s"); m_transitionDurationSpinner->setDecimals(1); m_transitionDurationSpinner->setMinimum(0); m_transitionDurationSpinner->setMaximum(10); m_transitionDurationSpinner->setValue(2); connect(m_transitionDurationSpinner, SIGNAL(valueChanged(double)), this, SLOT(on_parameterChanged())); grid->addWidget(m_transitionDurationSpinner, 3, 1); grid->addWidget(new QLabel(tr("Transition type")), 4, 0, Qt::AlignRight); m_transitionStyleCombo = new QComboBox(); m_transitionStyleCombo->addItem(tr("Random")); m_transitionStyleCombo->addItem(tr("Dissolve")); m_transitionStyleCombo->addItem(tr("Bar Horizontal")); m_transitionStyleCombo->addItem(tr("Bar Vertical")); m_transitionStyleCombo->addItem(tr("Barn Door Horizontal")); m_transitionStyleCombo->addItem(tr("Barn Door Vertical")); m_transitionStyleCombo->addItem(tr("Barn Door Diagonal SW-NE")); m_transitionStyleCombo->addItem(tr("Barn Door Diagonal NW-SE")); m_transitionStyleCombo->addItem(tr("Diagonal Top Left")); m_transitionStyleCombo->addItem(tr("Diagonal Top Right")); m_transitionStyleCombo->addItem(tr("Matrix Waterfall Horizontal")); m_transitionStyleCombo->addItem(tr("Matrix Waterfall Vertical")); m_transitionStyleCombo->addItem(tr("Matrix Snake Horizontal")); m_transitionStyleCombo->addItem(tr("Matrix Snake Parallel Horizontal")); m_transitionStyleCombo->addItem(tr("Matrix Snake Vertical")); m_transitionStyleCombo->addItem(tr("Matrix Snake Parallel Vertical")); m_transitionStyleCombo->addItem(tr("Barn V Up")); m_transitionStyleCombo->addItem(tr("Iris Circle")); m_transitionStyleCombo->addItem(tr("Double Iris")); m_transitionStyleCombo->addItem(tr("Iris Box")); m_transitionStyleCombo->addItem(tr("Box Bottom Right")); m_transitionStyleCombo->addItem(tr("Box Bottom Left")); m_transitionStyleCombo->addItem(tr("Box Right Center")); m_transitionStyleCombo->addItem(tr("Clock Top")); m_transitionStyleCombo->setToolTip(tr("Choose a transition effect.")); m_transitionStyleCombo->setCurrentIndex(1); connect(m_transitionStyleCombo, SIGNAL(currentIndexChanged(int)), this, SLOT(on_parameterChanged())); grid->addWidget(m_transitionStyleCombo, 4, 1); grid->addWidget(new QLabel(tr("Transition softness")), 5, 0, Qt::AlignRight); m_softnessSpinner = new QSpinBox(); m_softnessSpinner->setToolTip(tr("Change the softness of the edge of the wipe.")); m_softnessSpinner->setSuffix(" %"); m_softnessSpinner->setMaximum(100); m_softnessSpinner->setMinimum(0); m_softnessSpinner->setValue(20); connect(m_softnessSpinner, SIGNAL(valueChanged(int)), this, SLOT(on_parameterChanged())); grid->addWidget(m_softnessSpinner, 5, 1); m_preview = new ProducerPreviewWidget(MLT.profile().dar()); grid->addWidget(m_preview, 6, 0, 1, 2, Qt::AlignCenter); on_parameterChanged(); } SlideshowGeneratorWidget::~SlideshowGeneratorWidget() { m_future.waitForFinished(); m_preview->stop(); if (m_previewProducer) { delete m_previewProducer; } } Mlt::Playlist *SlideshowGeneratorWidget::getSlideshow() { SlideshowConfig config; m_mutex.lock(); // take a snapshot of the config. config = m_config; m_mutex.unlock(); int framesPerClip = round(config.clipDuration * MLT.profile().fps()); int count = m_clips->count(); Mlt::Playlist *slideshow = new Mlt::Playlist(MLT.profile()); Mlt::ClipInfo info; // Copy clips for (int i = 0; i < count; i++) { Mlt::ClipInfo *c = m_clips->clip_info(i, &info); if (c) { Mlt::Producer producer(MLT.profile(), "xml-string", MLT.XML(c->producer).toUtf8().constData()); slideshow->append(producer, c->frame_in, c->frame_in + framesPerClip - 1); } } // Add filters for (int i = 0; i < count; i++) { Mlt::ClipInfo *c = slideshow->clip_info(i, &info); if (c && c->producer) { if (!c->producer->property_exists("meta.media.width")) { delete c->producer->get_frame(); // makes avformat producer set meta.media.width and .height } attachAffineFilter(config, c->producer, c->frame_count - 1); attachBlurFilter(config, c->producer); } } // Add transitions int framesPerTransition = round(config.transitionDuration * MLT.profile().fps()); if (framesPerTransition > (framesPerClip / 2 - 1)) { framesPerTransition = (framesPerClip / 2 - 1); } if (framesPerTransition < minTransitionFrames) { framesPerTransition = 0; } if (framesPerTransition > 0) { for (int i = 0; i < count - 1; i++) { Mlt::ClipInfo *c = slideshow->clip_info(i, &info); if (c->frame_count < framesPerTransition) { // Do not add a transition if the first clip is too short continue; } c = slideshow->clip_info(i + 1, &info); if (c->frame_count < framesPerTransition) { // Do not add a transition if the second clip is too short continue; } // Create playlist mix slideshow->mix(i, framesPerTransition); QScopedPointer producer(slideshow->get_clip(i + 1)); if ( producer.isNull() ) { break; } producer->parent().set(kShotcutTransitionProperty, "lumaMix"); // Add mix transition Mlt::Transition crossFade(MLT.profile(), "mix:-1"); slideshow->mix_add(i + 1, &crossFade); // Add luma transition Mlt::Transition luma(MLT.profile(), Settings.playerGPU() ? "movit.luma_mix" : "luma"); applyLumaTransitionProperties(&luma, config); slideshow->mix_add(i + 1, &luma); count++; i++; } } return slideshow; } void SlideshowGeneratorWidget::attachAffineFilter(SlideshowConfig &config, Mlt::Producer *producer, int endPosition) { if (config.zoomPercent == 0 && config.aspectConversion != ASPECT_CONVERSION_CROP_CENTER && config.aspectConversion != ASPECT_CONVERSION_CROP_PAN) { return; } mlt_rect beginRect; mlt_rect endRect; beginRect.x = 0; beginRect.y = 0; beginRect.w = MLT.profile().width(); beginRect.h = MLT.profile().height(); beginRect.o = 1; endRect.x = beginRect.x; endRect.y = beginRect.y; endRect.w = beginRect.w; endRect.h = beginRect.h; endRect.o = 1; double destDar = MLT.profile().dar(); double sourceW = producer->get_double("meta.media.width"); double sourceH = producer->get_double("meta.media.height"); double sourceAr = producer->get_double("aspect_ratio"); double sourceDar = destDar; if (sourceW && sourceH && sourceAr) { sourceDar = sourceW * sourceAr / sourceH; } if (sourceDar == destDar && config.zoomPercent == 0) { // Aspect ratios match and no zoom. No need for affine. return; } if (config.aspectConversion == ASPECT_CONVERSION_CROP_CENTER || config.aspectConversion == ASPECT_CONVERSION_CROP_PAN) { if (sourceDar > destDar) { // Crop sides to fit height beginRect.w = (double)MLT.profile().width() * sourceDar / destDar; beginRect.h = MLT.profile().height(); beginRect.y = 0; endRect.w = beginRect.w; endRect.h = beginRect.h; endRect.y = beginRect.y; if (config.aspectConversion == ASPECT_CONVERSION_CROP_CENTER) { beginRect.x = ((double)MLT.profile().width() - beginRect.w) / 2.0; endRect.x = beginRect.x; } else { beginRect.x = 0; endRect.x = (double)MLT.profile().width() - endRect.w; } } else if (destDar > sourceDar) { // Crop top and bottom to fit width. beginRect.w = MLT.profile().width(); beginRect.h = (double)MLT.profile().height() * destDar / sourceDar; beginRect.x = 0; endRect.w = beginRect.w; endRect.h = beginRect.h; endRect.x = beginRect.x; if (config.aspectConversion == ASPECT_CONVERSION_CROP_CENTER) { beginRect.y = ((double)MLT.profile().height() - beginRect.h) / 2.0; endRect.y = beginRect.y; } else { beginRect.y = 0; endRect.y = (double)MLT.profile().height() - endRect.h; } } } else { // Pad: modify rect to fit the aspect ratio of the source if (sourceDar > destDar) { beginRect.w = MLT.profile().width(); beginRect.h = (double)MLT.profile().height() * destDar / sourceDar; beginRect.x = 0; beginRect.y = ((double)MLT.profile().height() - beginRect.h) / 2.0; } else if (destDar > sourceDar) { beginRect.w = (double)MLT.profile().width() * sourceDar / destDar; beginRect.h = MLT.profile().height(); beginRect.x = ((double)MLT.profile().width() - beginRect.w) / 2.0; beginRect.y = 0; } endRect.w = beginRect.w; endRect.h = beginRect.h; endRect.y = beginRect.y; endRect.x = beginRect.x; } if (config.zoomPercent > 0) { double endScale = (double)config.zoomPercent / 100.0; endRect.x = endRect.x - (endScale * endRect.w / 2.0); endRect.y = endRect.y - (endScale * endRect.h / 2.0); endRect.w = endRect.w + (endScale * endRect.w); endRect.h = endRect.h + (endScale * endRect.h); } else if (config.zoomPercent < 0) { double beginScale = -1.0 * (double)config.zoomPercent / 100.0; beginRect.x = beginRect.x - (beginScale * beginRect.w / 2.0); beginRect.y = beginRect.y - (beginScale * beginRect.h / 2.0); beginRect.w = beginRect.w + (beginScale * beginRect.w); beginRect.h = beginRect.h + (beginScale * beginRect.h); } Mlt::Filter filter(MLT.profile(), "affine"); filter.anim_set("transition.rect", beginRect, 0); filter.anim_set("transition.rect", endRect, endPosition); filter.set("transition.fill", 1); filter.set("transition.distort", 0); filter.set("transition.valign", "middle"); filter.set("transition.halign", "center"); filter.set("transition.threads", 0); filter.set("background", "color:#000000"); filter.set(kShotcutFilterProperty, "affineSizePosition"); filter.set(kShotcutAnimInProperty, producer->frames_to_time(endPosition + 1, mlt_time_clock)); filter.set(kShotcutAnimOutProperty, producer->frames_to_time(0, mlt_time_clock)); producer->attach(filter); } void SlideshowGeneratorWidget::attachBlurFilter(SlideshowConfig &config, Mlt::Producer *producer) { if (config.aspectConversion != ASPECT_CONVERSION_PAD_BLUR) { return; } mlt_rect rect; rect.x = 0; rect.y = 0; rect.w = MLT.profile().width(); rect.h = MLT.profile().height(); rect.o = 1; double destDar = MLT.profile().dar(); double sourceW = producer->get_double("meta.media.width"); double sourceH = producer->get_double("meta.media.height"); double sourceAr = producer->get_double("aspect_ratio"); double sourceDar = destDar; if ( sourceW && sourceH && sourceAr ) { sourceDar = sourceW * sourceAr / sourceH; } if (sourceDar == destDar) { // Aspect ratios match. No need for pad. return; } if (sourceDar > destDar) { // Blur top/bottom to pad. rect.h = MLT.profile().height() * destDar / sourceDar; rect.y = ((double)MLT.profile().height() - rect.h) / 2.0; } else if (destDar > sourceDar) { // Blur sides to pad. rect.w = MLT.profile().width() * sourceDar / destDar; rect.x = ((double)MLT.profile().width() - rect.w) / 2.0; } Mlt::Filter filter(MLT.profile(), "pillar_echo"); filter.set("rect", rect); filter.set("blur", 4); filter.set(kShotcutFilterProperty, "blur_pad"); producer->attach(filter); } void SlideshowGeneratorWidget::applyLumaTransitionProperties(Mlt::Transition *luma, SlideshowConfig &config) { int index = config.transitionStyle; if (index == 0) { // Random: pick any number other than 0 index = rand() % 24 + 1; } if (index == 1) { // Dissolve luma->set("resource", ""); } else { luma->set("resource", QString("%luma%1.pgm").arg(index - 1, 2, 10, QChar('0')).toLatin1().constData()); } luma->set("softness", config.transitionSoftness / 100.0); luma->set("progressive", 1); if (!Settings.playerGPU()) luma->set("alpha_over", 1); } void SlideshowGeneratorWidget::on_parameterChanged() { if (m_transitionDurationSpinner->value() > m_clipDurationSpinner->value() / 2 ) { m_transitionDurationSpinner->setValue(m_clipDurationSpinner->value() / 2); } if (m_transitionDurationSpinner->value() == 0) { m_transitionStyleCombo->setEnabled(false); m_softnessSpinner->setEnabled(false); } else if (m_transitionStyleCombo->currentIndex() == 1) { m_transitionStyleCombo->setEnabled(true); m_softnessSpinner->setEnabled(false); } else { m_transitionStyleCombo->setEnabled(true); m_softnessSpinner->setEnabled(true); } m_preview->stop(); m_preview->showText(tr("Generating Preview...")); m_mutex.lock(); m_refreshPreview = true; m_config.clipDuration = m_clipDurationSpinner->value(); m_config.aspectConversion = m_aspectConversionCombo->currentIndex(); m_config.zoomPercent = m_zoomPercentSpinner->value(); m_config.transitionDuration = m_transitionDurationSpinner->value(); m_config.transitionStyle = m_transitionStyleCombo->currentIndex(); m_config.transitionSoftness = m_softnessSpinner->value(); if (m_future.isFinished() || m_future.isCanceled()) { // Generate the preview producer in another thread because it can take some time m_future = QtConcurrent::run(this, &SlideshowGeneratorWidget::generatePreviewSlideshow); } m_mutex.unlock(); } void SlideshowGeneratorWidget::generatePreviewSlideshow() { m_mutex.lock(); while (m_refreshPreview) { m_refreshPreview = false; m_mutex.unlock(); Mlt::Producer *newProducer = getSlideshow(); m_mutex.lock(); if (!m_refreshPreview) { if (m_previewProducer) { delete m_previewProducer; } m_previewProducer = newProducer; QMetaObject::invokeMethod(this, "startPreview", Qt::QueuedConnection); } else { // Another refresh was requested while we generated this producer. // Delete it and make a new one. delete newProducer; } } m_mutex.unlock(); } void SlideshowGeneratorWidget::startPreview() { m_mutex.lock(); if (m_previewProducer) { m_preview->start(m_previewProducer); } m_previewProducer = nullptr; m_mutex.unlock(); }