diff --git a/src/bin/projectclip.cpp b/src/bin/projectclip.cpp index d8bf291fd..7cb3779ac 100644 --- a/src/bin/projectclip.cpp +++ b/src/bin/projectclip.cpp @@ -1,1588 +1,1588 @@ /* Copyright (C) 2012 Till Theato Copyright (C) 2014 Jean-Baptiste Mardelle This file is part of Kdenlive. See www.kdenlive.org. This program is free software; you can redistribute it and/or modify it under the terms of the GNU General Public License as published by the Free Software Foundation; either version 2 of the License or (at your option) version 3 or any later version accepted by the membership of KDE e.V. (or its successor approved by the membership of KDE e.V.), which shall act as a proxy defined in Section 14 of version 3 of the license. This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for more details. You should have received a copy of the GNU General Public License along with this program. If not, see . */ #include "projectclip.h" #include "bin.h" #include "core.h" #include "doc/docundostack.hpp" #include "doc/kdenlivedoc.h" #include "doc/kthumb.h" #include "effects/effectstack/model/effectstackmodel.hpp" #include "jobs/audiothumbjob.hpp" #include "jobs/jobmanager.h" #include "jobs/loadjob.hpp" #include "jobs/thumbjob.hpp" #include "jobs/cachejob.hpp" #include "kdenlivesettings.h" #include "lib/audio/audioStreamInfo.h" #include "mltcontroller/clipcontroller.h" #include "mltcontroller/clippropertiescontroller.h" #include "model/markerlistmodel.hpp" #include "profiles/profilemodel.hpp" #include "project/projectcommands.h" #include "project/projectmanager.h" #include "projectfolder.h" #include "projectitemmodel.h" #include "projectsubclip.h" #include "timecode.h" #include "timeline2/model/snapmodel.hpp" #include "utils/thumbnailcache.hpp" #include "xml/xml.hpp" #include #include #include #include "kdenlive_debug.h" #include "logger.hpp" #include #include #include #include #include #include #include #include #pragma GCC diagnostic push #pragma GCC diagnostic ignored "-Wunused-parameter" #pragma GCC diagnostic ignored "-Wsign-conversion" #pragma GCC diagnostic ignored "-Wfloat-equal" #pragma GCC diagnostic ignored "-Wshadow" #pragma GCC diagnostic ignored "-Wpedantic" #include #pragma GCC diagnostic pop RTTR_REGISTRATION { using namespace rttr; registration::class_("ProjectClip"); } ProjectClip::ProjectClip(const QString &id, const QIcon &thumb, const std::shared_ptr &model, std::shared_ptr producer) : AbstractProjectItem(AbstractProjectItem::ClipItem, id, model) , ClipController(id, std::move(producer)) { m_markerModel = std::make_shared(id, pCore->projectManager()->undoStack()); if (producer->get_int("_placeholder") == 1 || producer->get_int("_missingsource") == 1) { m_clipStatus = StatusMissing; } else { m_clipStatus = StatusReady; } m_name = clipName(); m_duration = getStringDuration(); m_inPoint = 0; m_outPoint = 0; m_date = date; m_description = ClipController::description(); if (m_clipType == ClipType::Audio) { m_thumbnail = QIcon::fromTheme(QStringLiteral("audio-x-generic")); } else { m_thumbnail = thumb; } // Make sure we have a hash for this clip hash(); connect(m_markerModel.get(), &MarkerListModel::modelChanged, [&]() { setProducerProperty(QStringLiteral("kdenlive:markers"), m_markerModel->toJson()); }); QString markers = getProducerProperty(QStringLiteral("kdenlive:markers")); if (!markers.isEmpty()) { QMetaObject::invokeMethod(m_markerModel.get(), "importFromJson", Qt::QueuedConnection, Q_ARG(const QString &, markers), Q_ARG(bool, true), Q_ARG(bool, false)); } setTags(getProducerProperty(QStringLiteral("kdenlive:tags"))); AbstractProjectItem::setRating((uint) getProducerIntProperty(QStringLiteral("kdenlive:rating"))); connectEffectStack(); } // static std::shared_ptr ProjectClip::construct(const QString &id, const QIcon &thumb, const std::shared_ptr &model, const std::shared_ptr &producer) { std::shared_ptr self(new ProjectClip(id, thumb, model, producer)); baseFinishConstruct(self); QMetaObject::invokeMethod(model.get(), "loadSubClips", Qt::QueuedConnection, Q_ARG(const QString&, id), Q_ARG(const QString&, self->getProducerProperty(QStringLiteral("kdenlive:clipzones")))); return self; } void ProjectClip::importEffects(const std::shared_ptr &producer) { m_effectStack->importEffects(producer, PlaylistState::Disabled, true); } ProjectClip::ProjectClip(const QString &id, const QDomElement &description, const QIcon &thumb, const std::shared_ptr &model) : AbstractProjectItem(AbstractProjectItem::ClipItem, id, model) , ClipController(id) { m_clipStatus = StatusWaiting; m_thumbnail = thumb; m_markerModel = std::make_shared(m_binId, pCore->projectManager()->undoStack()); if (description.hasAttribute(QStringLiteral("type"))) { m_clipType = (ClipType::ProducerType)description.attribute(QStringLiteral("type")).toInt(); if (m_clipType == ClipType::Audio) { m_thumbnail = QIcon::fromTheme(QStringLiteral("audio-x-generic")); } } m_temporaryUrl = getXmlProperty(description, QStringLiteral("resource")); QString clipName = getXmlProperty(description, QStringLiteral("kdenlive:clipname")); if (!clipName.isEmpty()) { m_name = clipName; } else if (!m_temporaryUrl.isEmpty()) { m_name = QFileInfo(m_temporaryUrl).fileName(); } else { m_name = i18n("Untitled"); } connect(m_markerModel.get(), &MarkerListModel::modelChanged, [&]() { setProducerProperty(QStringLiteral("kdenlive:markers"), m_markerModel->toJson()); }); } std::shared_ptr ProjectClip::construct(const QString &id, const QDomElement &description, const QIcon &thumb, std::shared_ptr model) { std::shared_ptr self(new ProjectClip(id, description, thumb, std::move(model))); baseFinishConstruct(self); return self; } ProjectClip::~ProjectClip() { // controller is deleted in bincontroller m_thumbMutex.lock(); m_requestedThumbs.clear(); m_thumbMutex.unlock(); m_thumbThread.waitForFinished(); } void ProjectClip::connectEffectStack() { connect(m_effectStack.get(), &EffectStackModel::dataChanged, [&]() { if (auto ptr = m_model.lock()) { std::static_pointer_cast(ptr)->onItemUpdated(std::static_pointer_cast(shared_from_this()), AbstractProjectItem::IconOverlay); } }); } QString ProjectClip::getToolTip() const { if (m_clipType == ClipType::Color && m_path.contains(QLatin1Char('/'))) { return m_path.section(QLatin1Char('/'), -1); } return m_path; } QString ProjectClip::getXmlProperty(const QDomElement &producer, const QString &propertyName, const QString &defaultValue) { QString value = defaultValue; QDomNodeList props = producer.elementsByTagName(QStringLiteral("property")); for (int i = 0; i < props.count(); ++i) { if (props.at(i).toElement().attribute(QStringLiteral("name")) == propertyName) { value = props.at(i).firstChild().nodeValue(); break; } } return value; } void ProjectClip::updateAudioThumbnail() { if (!KdenliveSettings::audiothumbnails()) { return; } m_audioThumbCreated = true; audioThumbReady(); updateTimelineClips({TimelineModel::ReloadThumbRole}); } bool ProjectClip::audioThumbCreated() const { return (m_audioThumbCreated); } ClipType::ProducerType ProjectClip::clipType() const { return m_clipType; } bool ProjectClip::hasParent(const QString &id) const { std::shared_ptr par = parent(); while (par) { if (par->clipId() == id) { return true; } par = par->parent(); } return false; } std::shared_ptr ProjectClip::clip(const QString &id) { if (id == m_binId) { return std::static_pointer_cast(shared_from_this()); } return std::shared_ptr(); } std::shared_ptr ProjectClip::folder(const QString &id) { Q_UNUSED(id) return std::shared_ptr(); } std::shared_ptr ProjectClip::getSubClip(int in, int out) { for (int i = 0; i < childCount(); ++i) { std::shared_ptr clip = std::static_pointer_cast(child(i))->subClip(in, out); if (clip) { return clip; } } return std::shared_ptr(); } QStringList ProjectClip::subClipIds() const { QStringList subIds; for (int i = 0; i < childCount(); ++i) { std::shared_ptr clip = std::static_pointer_cast(child(i)); if (clip) { subIds << clip->clipId(); } } return subIds; } std::shared_ptr ProjectClip::clipAt(int ix) { if (ix == row()) { return std::static_pointer_cast(shared_from_this()); } return std::shared_ptr(); } /*bool ProjectClip::isValid() const { return m_controller->isValid(); }*/ bool ProjectClip::hasUrl() const { if ((m_clipType != ClipType::Color) && (m_clipType != ClipType::Unknown)) { return (!clipUrl().isEmpty()); } return false; } const QString ProjectClip::url() const { return clipUrl(); } GenTime ProjectClip::duration() const { return getPlaytime(); } size_t ProjectClip::frameDuration() const { return (size_t)getFramePlaytime(); } void ProjectClip::reloadProducer(bool refreshOnly, bool audioStreamChanged, bool reloadAudio) { // we find if there are some loading job on that clip int loadjobId = -1; pCore->jobManager()->hasPendingJob(clipId(), AbstractClipJob::LOADJOB, &loadjobId); QMutexLocker lock(&m_thumbMutex); if (refreshOnly) { // In that case, we only want a new thumbnail. // We thus set up a thumb job. We must make sure that there is no pending LOADJOB // Clear cache first ThumbnailCache::get()->invalidateThumbsForClip(clipId(), false); pCore->jobManager()->discardJobs(clipId(), AbstractClipJob::THUMBJOB); m_thumbsProducer.reset(); pCore->jobManager()->startJob({clipId()}, loadjobId, QString(), -1, true, true); } else { // If another load job is running? if (loadjobId > -1) { pCore->jobManager()->discardJobs(clipId(), AbstractClipJob::LOADJOB); } if (QFile::exists(m_path) && !hasProxy()) { clearBackupProperties(); } QDomDocument doc; QDomElement xml = toXml(doc); if (!xml.isNull()) { pCore->jobManager()->discardJobs(clipId(), AbstractClipJob::THUMBJOB); m_thumbsProducer.reset(); ClipType::ProducerType type = clipType(); if (type != ClipType::Color && type != ClipType::Image && type != ClipType::SlideShow) { xml.removeAttribute("out"); } ThumbnailCache::get()->invalidateThumbsForClip(clipId(), reloadAudio); int loadJob = pCore->jobManager()->startJob({clipId()}, loadjobId, QString(), xml); pCore->jobManager()->startJob({clipId()}, loadJob, QString(), -1, true, true); if (audioStreamChanged) { discardAudioThumb(); pCore->jobManager()->startJob({clipId()}, loadjobId, QString()); } } } } QDomElement ProjectClip::toXml(QDomDocument &document, bool includeMeta, bool includeProfile) { getProducerXML(document, includeMeta, includeProfile); QDomElement prod; if (document.documentElement().tagName() == QLatin1String("producer")) { prod = document.documentElement(); } else { prod = document.documentElement().firstChildElement(QStringLiteral("producer")); } if (m_clipType != ClipType::Unknown) { prod.setAttribute(QStringLiteral("type"), (int)m_clipType); } return prod; } void ProjectClip::setThumbnail(const QImage &img) { if (img.isNull()) { return; } QPixmap thumb = roundedPixmap(QPixmap::fromImage(img)); if (hasProxy() && !thumb.isNull()) { // Overlay proxy icon QPainter p(&thumb); QColor c(220, 220, 10, 200); QRect r(0, 0, thumb.height() / 2.5, thumb.height() / 2.5); p.fillRect(r, c); QFont font = p.font(); font.setPixelSize(r.height()); font.setBold(true); p.setFont(font); p.setPen(Qt::black); p.drawText(r, Qt::AlignCenter, i18nc("The first letter of Proxy, used as abbreviation", "P")); } m_thumbnail = QIcon(thumb); if (auto ptr = m_model.lock()) { std::static_pointer_cast(ptr)->onItemUpdated(std::static_pointer_cast(shared_from_this()), AbstractProjectItem::DataThumbnail); } } bool ProjectClip::hasAudioAndVideo() const { return hasAudio() && hasVideo() && m_masterProducer->get_int("set.test_image") == 0 && m_masterProducer->get_int("set.test_audio") == 0; } bool ProjectClip::isCompatible(PlaylistState::ClipState state) const { switch (state) { case PlaylistState::AudioOnly: return hasAudio() && (m_masterProducer->get_int("set.test_audio") == 0); case PlaylistState::VideoOnly: return hasVideo() && (m_masterProducer->get_int("set.test_image") == 0); default: return true; } } QPixmap ProjectClip::thumbnail(int width, int height) { return m_thumbnail.pixmap(width, height); } bool ProjectClip::setProducer(std::shared_ptr producer, bool replaceProducer) { Q_UNUSED(replaceProducer) qDebug() << "################### ProjectClip::setproducer"; QMutexLocker locker(&m_producerMutex); updateProducer(producer); m_thumbsProducer.reset(); connectEffectStack(); // Update info if (m_name.isEmpty()) { m_name = clipName(); } m_date = date; m_description = ClipController::description(); m_temporaryUrl.clear(); if (m_clipType == ClipType::Audio) { m_thumbnail = QIcon::fromTheme(QStringLiteral("audio-x-generic")); } else if (m_clipType == ClipType::Image) { if (producer->get_int("meta.media.width") < 8 || producer->get_int("meta.media.height") < 8) { KMessageBox::information(QApplication::activeWindow(), i18n("Image dimension smaller than 8 pixels.\nThis is not correctly supported by our video framework.")); } } m_duration = getStringDuration(); m_clipStatus = StatusReady; setTags(getProducerProperty(QStringLiteral("kdenlive:tags"))); AbstractProjectItem::setRating((uint) getProducerIntProperty(QStringLiteral("kdenlive:rating"))); if (!hasProxy()) { if (auto ptr = m_model.lock()) emit std::static_pointer_cast(ptr)->refreshPanel(m_binId); } if (auto ptr = m_model.lock()) { std::static_pointer_cast(ptr)->onItemUpdated(std::static_pointer_cast(shared_from_this()), AbstractProjectItem::DataDuration); std::static_pointer_cast(ptr)->updateWatcher(std::static_pointer_cast(shared_from_this())); } // Make sure we have a hash for this clip getFileHash(); // set parent again (some info need to be stored in producer) updateParent(parentItem().lock()); if (pCore->currentDoc()->getDocumentProperty(QStringLiteral("enableproxy")).toInt() == 1) { QList> clipList; // automatic proxy generation enabled if (m_clipType == ClipType::Image && pCore->currentDoc()->getDocumentProperty(QStringLiteral("generateimageproxy")).toInt() == 1) { if (getProducerIntProperty(QStringLiteral("meta.media.width")) >= KdenliveSettings::proxyimageminsize() && getProducerProperty(QStringLiteral("kdenlive:proxy")) == QStringLiteral()) { clipList << std::static_pointer_cast(shared_from_this()); } } else if (pCore->currentDoc()->getDocumentProperty(QStringLiteral("generateproxy")).toInt() == 1 && (m_clipType == ClipType::AV || m_clipType == ClipType::Video) && getProducerProperty(QStringLiteral("kdenlive:proxy")) == QStringLiteral()) { bool skipProducer = false; if (pCore->currentDoc()->getDocumentProperty(QStringLiteral("enableexternalproxy")).toInt() == 1) { QStringList externalParams = pCore->currentDoc()->getDocumentProperty(QStringLiteral("externalproxyparams")).split(QLatin1Char(';')); // We have a camcorder profile, check if we have opened a proxy clip if (externalParams.count() >= 6) { QFileInfo info(m_path); QDir dir = info.absoluteDir(); dir.cd(externalParams.at(3)); QString fileName = info.fileName(); if (!externalParams.at(2).isEmpty()) { fileName.chop(externalParams.at(2).size()); } fileName.append(externalParams.at(5)); if (dir.exists(fileName)) { setProducerProperty(QStringLiteral("kdenlive:proxy"), m_path); m_path = dir.absoluteFilePath(fileName); setProducerProperty(QStringLiteral("kdenlive:originalurl"), m_path); getFileHash(); skipProducer = true; } } } if (!skipProducer && getProducerIntProperty(QStringLiteral("meta.media.width")) >= KdenliveSettings::proxyminsize()) { clipList << std::static_pointer_cast(shared_from_this()); } } if (!clipList.isEmpty()) { pCore->currentDoc()->slotProxyCurrentItem(true, clipList, false); } } pCore->bin()->reloadMonitorIfActive(clipId()); for (auto &p : m_audioProducers) { m_effectStack->removeService(p.second); } for (auto &p : m_videoProducers) { m_effectStack->removeService(p.second); } for (auto &p : m_timewarpProducers) { m_effectStack->removeService(p.second); } // Release audio producers m_audioProducers.clear(); m_videoProducers.clear(); m_timewarpProducers.clear(); emit refreshPropertiesPanel(); if (m_clipType == ClipType::AV || m_clipType == ClipType::Video || m_clipType == ClipType::Playlist) { QTimer::singleShot(1000, this, [this]() { int loadjobId; if (!pCore->jobManager()->hasPendingJob(m_binId, AbstractClipJob::CACHEJOB, &loadjobId)) { pCore->jobManager()->startJob({m_binId}, -1, QString()); } }); } replaceInTimeline(); updateTimelineClips({TimelineModel::IsProxyRole}); return true; } std::shared_ptr ProjectClip::thumbProducer() { if (m_thumbsProducer) { return m_thumbsProducer; } if (clipType() == ClipType::Unknown) { return nullptr; } QMutexLocker lock(&m_thumbMutex); std::shared_ptr prod = originalProducer(); if (!prod->is_valid()) { return nullptr; } if (KdenliveSettings::gpu_accel()) { // TODO: when the original producer changes, we must reload this thumb producer m_thumbsProducer = softClone(ClipController::getPassPropertiesList()); } else { QString mltService = m_masterProducer->get("mlt_service"); const QString mltResource = m_masterProducer->get("resource"); if (mltService == QLatin1String("avformat")) { mltService = QStringLiteral("avformat-novalidate"); } m_thumbsProducer.reset(new Mlt::Producer(*pCore->thumbProfile(), mltService.toUtf8().constData(), mltResource.toUtf8().constData())); if (m_thumbsProducer->is_valid()) { Mlt::Properties original(m_masterProducer->get_properties()); Mlt::Properties cloneProps(m_thumbsProducer->get_properties()); cloneProps.pass_list(original, ClipController::getPassPropertiesList()); Mlt::Filter scaler(*pCore->thumbProfile(), "swscale"); Mlt::Filter padder(*pCore->thumbProfile(), "resize"); Mlt::Filter converter(*pCore->thumbProfile(), "avcolor_space"); m_thumbsProducer->set("audio_index", -1); // Required to make get_playtime() return > 1 m_thumbsProducer->set("out", m_thumbsProducer->get_length() -1); m_thumbsProducer->attach(scaler); m_thumbsProducer->attach(padder); m_thumbsProducer->attach(converter); } } return m_thumbsProducer; } void ProjectClip::createDisabledMasterProducer() { if (!m_disabledProducer) { m_disabledProducer = cloneProducer(); m_disabledProducer->set("set.test_audio", 1); m_disabledProducer->set("set.test_image", 1); m_effectStack->addService(m_disabledProducer); } } std::shared_ptr ProjectClip::getTimelineProducer(int trackId, int clipId, PlaylistState::ClipState state, int audioStream, double speed) { if (!m_masterProducer) { return nullptr; } if (qFuzzyCompare(speed, 1.0)) { // we are requesting a normal speed producer bool byPassTrackProducer = false; if (trackId == -1 && (state != PlaylistState::AudioOnly || audioStream == m_masterProducer->get_int("audio_index"))) { byPassTrackProducer = true; } if (byPassTrackProducer || (state == PlaylistState::VideoOnly && (m_clipType == ClipType::Color || m_clipType == ClipType::Image || m_clipType == ClipType::Text|| m_clipType == ClipType::TextTemplate || m_clipType == ClipType::Qml))) { // Temporary copy, return clone of master int duration = m_masterProducer->time_to_frames(m_masterProducer->get("kdenlive:duration")); return std::shared_ptr(m_masterProducer->cut(-1, duration > 0 ? duration - 1 : -1)); } if (m_timewarpProducers.count(clipId) > 0) { m_effectStack->removeService(m_timewarpProducers[clipId]); m_timewarpProducers.erase(clipId); } if (state == PlaylistState::AudioOnly) { // We need to get an audio producer, if none exists if (audioStream > -1) { if (trackId >= 0) { trackId += 100 * audioStream; } else { trackId -= 100 * audioStream; } } if (m_audioProducers.count(trackId) == 0) { m_audioProducers[trackId] = cloneProducer(true); m_audioProducers[trackId]->set("set.test_audio", 0); m_audioProducers[trackId]->set("set.test_image", 1); if (audioStream > -1) { m_audioProducers[trackId]->set("audio_index", audioStream); } m_effectStack->addService(m_audioProducers[trackId]); } return std::shared_ptr(m_audioProducers[trackId]->cut()); } if (m_audioProducers.count(trackId) > 0) { m_effectStack->removeService(m_audioProducers[trackId]); m_audioProducers.erase(trackId); } if (state == PlaylistState::VideoOnly) { // we return the video producer // We need to get an audio producer, if none exists if (m_videoProducers.count(trackId) == 0) { m_videoProducers[trackId] = cloneProducer(true); m_videoProducers[trackId]->set("set.test_audio", 1); m_videoProducers[trackId]->set("set.test_image", 0); m_effectStack->addService(m_videoProducers[trackId]); } int duration = m_masterProducer->time_to_frames(m_masterProducer->get("kdenlive:duration")); return std::shared_ptr(m_videoProducers[trackId]->cut(-1, duration > 0 ? duration - 1: -1)); } if (m_videoProducers.count(trackId) > 0) { m_effectStack->removeService(m_videoProducers[trackId]); m_videoProducers.erase(trackId); } Q_ASSERT(state == PlaylistState::Disabled); createDisabledMasterProducer(); int duration = m_masterProducer->time_to_frames(m_masterProducer->get("kdenlive:duration")); return std::shared_ptr(m_disabledProducer->cut(-1, duration > 0 ? duration - 1: -1)); } // For timewarp clips, we keep one separate producer for each clip. std::shared_ptr warpProducer; if (m_timewarpProducers.count(clipId) > 0) { // remove in all cases, we add it unconditionally anyways m_effectStack->removeService(m_timewarpProducers[clipId]); if (qFuzzyCompare(m_timewarpProducers[clipId]->get_double("warp_speed"), speed)) { // the producer we have is good, use it ! warpProducer = m_timewarpProducers[clipId]; qDebug() << "Reusing producer!"; } else { m_timewarpProducers.erase(clipId); } } if (!warpProducer) { QString resource(originalProducer()->get("resource")); if (resource.isEmpty() || resource == QLatin1String("")) { resource = m_service; } QString url = QString("timewarp:%1:%2").arg(QString::fromStdString(std::to_string(speed))).arg(resource); warpProducer.reset(new Mlt::Producer(*originalProducer()->profile(), url.toUtf8().constData())); qDebug() << "new producer: " << url; qDebug() << "warp LENGTH before" << warpProducer->get_length(); int original_length = originalProducer()->get_length(); // this is a workaround to cope with Mlt erroneous rounding Mlt::Properties original(m_masterProducer->get_properties()); Mlt::Properties cloneProps(warpProducer->get_properties()); cloneProps.pass_list(original, ClipController::getPassPropertiesList(false)); warpProducer->set("length", (int) (original_length / std::abs(speed) + 0.5)); } qDebug() << "warp LENGTH" << warpProducer->get_length(); warpProducer->set("set.test_audio", 1); warpProducer->set("set.test_image", 1); if (state == PlaylistState::AudioOnly) { warpProducer->set("set.test_audio", 0); } if (state == PlaylistState::VideoOnly) { warpProducer->set("set.test_image", 0); } m_timewarpProducers[clipId] = warpProducer; m_effectStack->addService(m_timewarpProducers[clipId]); return std::shared_ptr(warpProducer->cut()); } std::pair, bool> ProjectClip::giveMasterAndGetTimelineProducer(int clipId, std::shared_ptr master, PlaylistState::ClipState state, int tid) { int in = master->get_in(); int out = master->get_out(); if (master->parent().is_valid()) { // in that case, we have a cut // check whether it's a timewarp double speed = 1.0; bool timeWarp = false; if (QString::fromUtf8(master->parent().get("mlt_service")) == QLatin1String("timewarp")) { speed = master->parent().get_double("warp_speed"); timeWarp = true; } if (master->parent().get_int("_loaded") == 1) { // we already have a clip that shares the same master if (state != PlaylistState::Disabled || timeWarp) { // In that case, we must create copies std::shared_ptr prod(getTimelineProducer(tid, clipId, state, master->parent().get_int("audio_index"), speed)->cut(in, out)); return {prod, false}; } if (state == PlaylistState::Disabled) { if (!m_disabledProducer) { qDebug() << "Warning: weird, we found a disabled clip whose master is already loaded but we don't have any yet"; createDisabledMasterProducer(); } return {std::shared_ptr(m_disabledProducer->cut(in, out)), false}; } // We have a good id, this clip can be used return {master, true}; } else { master->parent().set("_loaded", 1); if (timeWarp) { m_timewarpProducers[clipId] = std::make_shared(&master->parent()); m_effectStack->loadService(m_timewarpProducers[clipId]); return {master, true}; } if (state == PlaylistState::AudioOnly) { int producerId = tid; int audioStream = master->parent().get_int("audio_index"); if (audioStream > -1) { producerId += 100 * audioStream; } m_audioProducers[tid] = std::make_shared(&master->parent()); m_effectStack->loadService(m_audioProducers[tid]); return {master, true}; } if (state == PlaylistState::VideoOnly) { // good, we found a master video producer, and we didn't have any if (m_clipType != ClipType::Color && m_clipType != ClipType::Image && m_clipType != ClipType::Text) { // Color, image and text clips always use master producer in timeline m_videoProducers[tid] = std::make_shared(&master->parent()); m_effectStack->loadService(m_videoProducers[tid]); } return {master, true}; } if (state == PlaylistState::Disabled) { if (!m_disabledProducer) { createDisabledMasterProducer(); } return {std::make_shared(m_disabledProducer->cut(master->get_in(), master->get_out())), true}; } qDebug() << "Warning: weird, we found a clip whose master is not loaded but we already have a master"; Q_ASSERT(false); } } else if (master->is_valid()) { // in that case, we have a master qDebug() << "Warning: weird, we received a master clip in lieue of a cut"; double speed = 1.0; if (QString::fromUtf8(master->parent().get("mlt_service")) == QLatin1String("timewarp")) { speed = master->get_double("warp_speed"); } return {getTimelineProducer(-1, clipId, state, master->get_int("audio_index"), speed), false}; } // we have a problem return {std::shared_ptr(ClipController::mediaUnavailable->cut()), false}; } std::shared_ptr ProjectClip::cloneProducer(bool removeEffects) { Mlt::Consumer c(pCore->getCurrentProfile()->profile(), "xml", "string"); Mlt::Service s(m_masterProducer->get_service()); int ignore = s.get_int("ignore_points"); if (ignore) { s.set("ignore_points", 0); } c.connect(s); c.set("time_format", "frames"); c.set("no_meta", 1); c.set("no_root", 1); c.set("no_profile", 1); c.set("root", "/"); c.set("store", "kdenlive"); c.run(); if (ignore) { s.set("ignore_points", ignore); } const QByteArray clipXml = c.get("string"); std::shared_ptr prod; prod.reset(new Mlt::Producer(pCore->getCurrentProfile()->profile(), "xml-string", clipXml.constData())); if (strcmp(prod->get("mlt_service"), "avformat") == 0) { prod->set("mlt_service", "avformat-novalidate"); prod->set("mute_on_pause", 0); } // we pass some properties that wouldn't be passed because of the novalidate const char *prefix = "meta."; const size_t prefix_len = strlen(prefix); for (int i = 0; i < m_masterProducer->count(); ++i) { char *current = m_masterProducer->get_name(i); if (strlen(current) >= prefix_len && strncmp(current, prefix, prefix_len) == 0) { prod->set(current, m_masterProducer->get(i)); } } if (removeEffects) { int ct = 0; Mlt::Filter *filter = prod->filter(ct); while (filter) { qDebug() << "// EFFECT " << ct << " : " << filter->get("mlt_service"); QString ix = QString::fromLatin1(filter->get("kdenlive_id")); if (!ix.isEmpty()) { qDebug() << "/ + + DELETING"; if (prod->detach(*filter) == 0) { } else { ct++; } } else { ct++; } delete filter; filter = prod->filter(ct); } } prod->set("id", (char *)nullptr); return prod; } std::shared_ptr ProjectClip::cloneProducer(const std::shared_ptr &producer) { Mlt::Consumer c(*producer->profile(), "xml", "string"); Mlt::Service s(producer->get_service()); int ignore = s.get_int("ignore_points"); if (ignore) { s.set("ignore_points", 0); } c.connect(s); c.set("time_format", "frames"); c.set("no_meta", 1); c.set("no_root", 1); c.set("no_profile", 1); c.set("root", "/"); c.set("store", "kdenlive"); c.start(); if (ignore) { s.set("ignore_points", ignore); } const QByteArray clipXml = c.get("string"); std::shared_ptr prod(new Mlt::Producer(*producer->profile(), "xml-string", clipXml.constData())); if (strcmp(prod->get("mlt_service"), "avformat") == 0) { prod->set("mlt_service", "avformat-novalidate"); prod->set("mute_on_pause", 0); } return prod; } std::shared_ptr ProjectClip::softClone(const char *list) { QString service = QString::fromLatin1(m_masterProducer->get("mlt_service")); QString resource = QString::fromUtf8(m_masterProducer->get("resource")); std::shared_ptr clone(new Mlt::Producer(*pCore->thumbProfile(), service.toUtf8().constData(), resource.toUtf8().constData())); Mlt::Filter scaler(*pCore->thumbProfile(), "swscale"); Mlt::Filter converter(pCore->getCurrentProfile()->profile(), "avcolor_space"); clone->attach(scaler); clone->attach(converter); Mlt::Properties original(m_masterProducer->get_properties()); Mlt::Properties cloneProps(clone->get_properties()); cloneProps.pass_list(original, list); return clone; } std::unique_ptr ProjectClip::getClone() { const char *list = ClipController::getPassPropertiesList(); QString service = QString::fromLatin1(m_masterProducer->get("mlt_service")); QString resource = QString::fromUtf8(m_masterProducer->get("resource")); std::unique_ptr clone(new Mlt::Producer(*m_masterProducer->profile(), service.toUtf8().constData(), resource.toUtf8().constData())); Mlt::Properties original(m_masterProducer->get_properties()); Mlt::Properties cloneProps(clone->get_properties()); cloneProps.pass_list(original, list); return clone; } bool ProjectClip::isReady() const { return m_clipStatus == StatusReady; } QPoint ProjectClip::zone() const { return ClipController::zone(); } const QString ProjectClip::hash() { QString clipHash = getProducerProperty(QStringLiteral("kdenlive:file_hash")); if (!clipHash.isEmpty()) { return clipHash; } return getFileHash(); } const QString ProjectClip::getFileHash() { QByteArray fileData; QByteArray fileHash; switch (m_clipType) { case ClipType::SlideShow: fileData = clipUrl().toUtf8(); fileHash = QCryptographicHash::hash(fileData, QCryptographicHash::Md5); break; case ClipType::Text: fileData = getProducerProperty(QStringLiteral("xmldata")).toUtf8(); fileHash = QCryptographicHash::hash(fileData, QCryptographicHash::Md5); break; case ClipType::TextTemplate: fileData = getProducerProperty(QStringLiteral("resource")).toUtf8(); fileData.append(getProducerProperty(QStringLiteral("templatetext")).toUtf8()); fileHash = QCryptographicHash::hash(fileData, QCryptographicHash::Md5); break; case ClipType::QText: fileData = getProducerProperty(QStringLiteral("text")).toUtf8(); fileHash = QCryptographicHash::hash(fileData, QCryptographicHash::Md5); break; case ClipType::Color: fileData = getProducerProperty(QStringLiteral("resource")).toUtf8(); fileHash = QCryptographicHash::hash(fileData, QCryptographicHash::Md5); break; default: QFile file(clipUrl()); if (file.open(QIODevice::ReadOnly)) { // write size and hash only if resource points to a file /* * 1 MB = 1 second per 450 files (or faster) * 10 MB = 9 seconds per 450 files (or faster) */ if (file.size() > 2000000) { fileData = file.read(1000000); if (file.seek(file.size() - 1000000)) { fileData.append(file.readAll()); } } else { fileData = file.readAll(); } file.close(); ClipController::setProducerProperty(QStringLiteral("kdenlive:file_size"), QString::number(file.size())); fileHash = QCryptographicHash::hash(fileData, QCryptographicHash::Md5); } break; } if (fileHash.isEmpty()) { qDebug() << "// WARNING EMPTY CLIP HASH: "; return QString(); } QString result = fileHash.toHex(); ClipController::setProducerProperty(QStringLiteral("kdenlive:file_hash"), result); return result; } double ProjectClip::getOriginalFps() const { return originalFps(); } bool ProjectClip::hasProxy() const { QString proxy = getProducerProperty(QStringLiteral("kdenlive:proxy")); return proxy.size() > 2; } void ProjectClip::setProperties(const QMap &properties, bool refreshPanel) { qDebug() << "// SETTING CLIP PROPERTIES: " << properties; QMapIterator i(properties); QMap passProperties; bool refreshAnalysis = false; bool reload = false; bool refreshOnly = true; if (properties.contains(QStringLiteral("templatetext"))) { m_description = properties.value(QStringLiteral("templatetext")); if (auto ptr = m_model.lock()) std::static_pointer_cast(ptr)->onItemUpdated(std::static_pointer_cast(shared_from_this()), AbstractProjectItem::ClipStatus); refreshPanel = true; } // Some properties also need to be passed to track producers QStringList timelineProperties{ QStringLiteral("force_aspect_ratio"), QStringLiteral("set.force_full_luma"), QStringLiteral("full_luma"), QStringLiteral("threads"), QStringLiteral("force_colorspace"), QStringLiteral("force_tff"), QStringLiteral("force_progressive"), QStringLiteral("video_delay") }; QStringList forceReloadProperties{QStringLiteral("autorotate"), QStringLiteral("templatetext"), QStringLiteral("resource"), QStringLiteral("force_fps"), QStringLiteral("set.test_image"), QStringLiteral("video_index")}; QStringList keys{QStringLiteral("luma_duration"), QStringLiteral("luma_file"), QStringLiteral("fade"), QStringLiteral("ttl"), QStringLiteral("softness"), QStringLiteral("crop"), QStringLiteral("animation")}; QVector updateRoles; while (i.hasNext()) { i.next(); setProducerProperty(i.key(), i.value()); if (m_clipType == ClipType::SlideShow && keys.contains(i.key())) { reload = true; refreshOnly = false; } if (i.key().startsWith(QLatin1String("kdenlive:clipanalysis"))) { refreshAnalysis = true; } if (timelineProperties.contains(i.key())) { passProperties.insert(i.key(), i.value()); } } if (properties.contains(QStringLiteral("kdenlive:proxy"))) { QString value = properties.value(QStringLiteral("kdenlive:proxy")); // If value is "-", that means user manually disabled proxy on this clip if (value.isEmpty() || value == QLatin1String("-")) { // reset proxy int id; if (pCore->jobManager()->hasPendingJob(clipId(), AbstractClipJob::PROXYJOB, &id)) { // The proxy clip is being created, abort pCore->jobManager()->discardJobs(clipId(), AbstractClipJob::PROXYJOB); } else { reload = true; refreshOnly = false; } } else { // A proxy was requested, make sure to keep original url setProducerProperty(QStringLiteral("kdenlive:originalurl"), url()); backupOriginalProperties(); pCore->jobManager()->startJob({clipId()}, -1, QString()); } } else if (!reload) { const QList propKeys = properties.keys(); for (const QString &k : propKeys) { if (forceReloadProperties.contains(k)) { refreshPanel = true; reload = true; if (m_clipType == ClipType::Color) { refreshOnly = true; updateRoles << TimelineModel::ResourceRole; } else { // Clip resource changed, update thumbnail, name, clear hash refreshOnly = false; if (propKeys.contains(QStringLiteral("resource"))) { resetProducerProperty(QStringLiteral("kdenlive:file_hash")); setProducerProperty(QStringLiteral("kdenlive:originalurl"), url()); updateRoles << TimelineModel::ResourceRole << TimelineModel::MaxDurationRole << TimelineModel::NameRole; } } break; } } } if (!reload && (properties.contains(QStringLiteral("xmldata")) || !passProperties.isEmpty())) { reload = true; } if (refreshAnalysis) { emit refreshAnalysisPanel(); } if (properties.contains(QStringLiteral("length")) || properties.contains(QStringLiteral("kdenlive:duration"))) { // Make sure length is >= kdenlive:duration int producerLength = getProducerIntProperty(QStringLiteral("length")); int kdenliveLength = getFramePlaytime(); if (producerLength < kdenliveLength) { setProducerProperty(QStringLiteral("length"), kdenliveLength); } m_duration = getStringDuration(); if (auto ptr = m_model.lock()) std::static_pointer_cast(ptr)->onItemUpdated(std::static_pointer_cast(shared_from_this()), AbstractProjectItem::DataDuration); refreshOnly = false; reload = true; } if (properties.contains(QStringLiteral("kdenlive:tags"))) { setTags(properties.value(QStringLiteral("kdenlive:tags"))); if (auto ptr = m_model.lock()) { std::static_pointer_cast(ptr)->onItemUpdated(std::static_pointer_cast(shared_from_this()), AbstractProjectItem::DataTag); } } if (properties.contains(QStringLiteral("kdenlive:clipname"))) { m_name = properties.value(QStringLiteral("kdenlive:clipname")); refreshPanel = true; if (auto ptr = m_model.lock()) { std::static_pointer_cast(ptr)->onItemUpdated(std::static_pointer_cast(shared_from_this()), AbstractProjectItem::DataName); } // update timeline clips if (!reload) { updateTimelineClips({TimelineModel::NameRole}); } } bool audioStreamChanged = properties.contains(QStringLiteral("audio_index")); if (reload) { // producer has changed, refresh monitor and thumbnail if (hasProxy()) { pCore->jobManager()->discardJobs(clipId(), AbstractClipJob::PROXYJOB); setProducerProperty(QStringLiteral("_overwriteproxy"), 1); pCore->jobManager()->startJob({clipId()}, -1, QString()); } else { reloadProducer(refreshOnly, audioStreamChanged, audioStreamChanged || (!refreshOnly && !properties.contains(QStringLiteral("kdenlive:proxy")))); } if (refreshOnly) { if (auto ptr = m_model.lock()) { emit std::static_pointer_cast(ptr)->refreshClip(m_binId); } } if (!updateRoles.isEmpty()) { updateTimelineClips(updateRoles); } } else { if (properties.contains(QStringLiteral("kdenlive:active_streams")) && m_audioInfo) { // Clip is a multi audio stream and currently in clip monitor, update target tracks m_audioInfo->updateActiveStreams(properties.value(QStringLiteral("kdenlive:active_streams"))); pCore->bin()->updateTargets(clipId()); if (!audioStreamChanged) { pCore->bin()->reloadMonitorStreamIfActive(clipId()); refreshPanel = true; } } if (audioStreamChanged) { refreshAudioInfo(); audioThumbReady(); pCore->bin()->reloadMonitorStreamIfActive(clipId()); refreshPanel = true; } } if (refreshPanel) { // Some of the clip properties have changed through a command, update properties panel emit refreshPropertiesPanel(); } if (!passProperties.isEmpty() && (!reload || refreshOnly)) { for (auto &p : m_audioProducers) { QMapIterator pr(passProperties); while (pr.hasNext()) { pr.next(); p.second->set(pr.key().toUtf8().constData(), pr.value().toUtf8().constData()); } } for (auto &p : m_videoProducers) { QMapIterator pr(passProperties); while (pr.hasNext()) { pr.next(); p.second->set(pr.key().toUtf8().constData(), pr.value().toUtf8().constData()); } } for (auto &p : m_timewarpProducers) { QMapIterator pr(passProperties); while (pr.hasNext()) { pr.next(); p.second->set(pr.key().toUtf8().constData(), pr.value().toUtf8().constData()); } } } } ClipPropertiesController *ProjectClip::buildProperties(QWidget *parent) { auto ptr = m_model.lock(); Q_ASSERT(ptr); auto *panel = new ClipPropertiesController(static_cast(this), parent); connect(this, &ProjectClip::refreshPropertiesPanel, panel, &ClipPropertiesController::slotReloadProperties); connect(this, &ProjectClip::refreshAnalysisPanel, panel, &ClipPropertiesController::slotFillAnalysisData); connect(panel, &ClipPropertiesController::requestProxy, [this](bool doProxy) { QList> clipList{std::static_pointer_cast(shared_from_this())}; pCore->currentDoc()->slotProxyCurrentItem(doProxy, clipList); }); connect(panel, &ClipPropertiesController::deleteProxy, this, &ProjectClip::deleteProxy); return panel; } void ProjectClip::deleteProxy() { // Disable proxy file QString proxy = getProducerProperty(QStringLiteral("kdenlive:proxy")); QList> clipList{std::static_pointer_cast(shared_from_this())}; pCore->currentDoc()->slotProxyCurrentItem(false, clipList); // Delete bool ok; QDir dir = pCore->currentDoc()->getCacheDir(CacheProxy, &ok); if (ok && proxy.length() > 2) { proxy = QFileInfo(proxy).fileName(); if (dir.exists(proxy)) { dir.remove(proxy); } } } void ProjectClip::updateParent(std::shared_ptr parent) { if (parent) { auto item = std::static_pointer_cast(parent); ClipController::setProducerProperty(QStringLiteral("kdenlive:folderid"), item->clipId()); } AbstractProjectItem::updateParent(parent); } bool ProjectClip::matches(const QString &condition) { // TODO Q_UNUSED(condition) return true; } bool ProjectClip::rename(const QString &name, int column) { QMap newProperites; QMap oldProperites; bool edited = false; switch (column) { case 0: if (m_name == name) { return false; } // Rename clip oldProperites.insert(QStringLiteral("kdenlive:clipname"), m_name); newProperites.insert(QStringLiteral("kdenlive:clipname"), name); m_name = name; edited = true; break; case 2: if (m_description == name) { return false; } // Rename clip if (m_clipType == ClipType::TextTemplate) { oldProperites.insert(QStringLiteral("templatetext"), m_description); newProperites.insert(QStringLiteral("templatetext"), name); } else { oldProperites.insert(QStringLiteral("kdenlive:description"), m_description); newProperites.insert(QStringLiteral("kdenlive:description"), name); } m_description = name; edited = true; break; } if (edited) { pCore->bin()->slotEditClipCommand(m_binId, oldProperites, newProperites); } return edited; } QVariant ProjectClip::getData(DataType type) const { switch (type) { case AbstractProjectItem::IconOverlay: if (m_clipStatus == AbstractProjectItem::StatusMissing) { return QVariant("window-close"); } if (m_clipStatus == AbstractProjectItem::StatusWaiting) { return QVariant("view-refresh"); } return m_effectStack && m_effectStack->rowCount() > 0 ? QVariant("kdenlive-track_has_effect") : QVariant(); default: return AbstractProjectItem::getData(type); } } int ProjectClip::audioChannels() const { if (!audioInfo()) { return 0; } return audioInfo()->channels(); } void ProjectClip::discardAudioThumb() { if (!m_audioInfo) { return; } QString audioThumbPath = getAudioThumbPath(audioInfo()->ffmpeg_audio_index()); if (!audioThumbPath.isEmpty()) { QFile::remove(audioThumbPath); } qCDebug(KDENLIVE_LOG) << "//////////////////// DISCARD AUDIO THUMBS"; m_audioThumbCreated = false; refreshAudioInfo(); pCore->jobManager()->discardJobs(clipId(), AbstractClipJob::AUDIOTHUMBJOB); } int ProjectClip::getAudioStreamFfmpegIndex(int mltStream) { if (!m_masterProducer || !audioInfo()) { return -1; } QList audioStreams = audioInfo()->streams().keys(); if (audioStreams.contains(mltStream)) { return audioStreams.indexOf(mltStream); } return -1; } const QString ProjectClip::getAudioThumbPath(int stream, bool miniThumb) { if (audioInfo() == nullptr && !miniThumb) { return QString(); } bool ok = false; QDir thumbFolder = pCore->currentDoc()->getCacheDir(CacheAudio, &ok); if (!ok) { return QString(); } const QString clipHash = hash(); if (clipHash.isEmpty()) { return QString(); } QString audioPath = thumbFolder.absoluteFilePath(clipHash); audioPath.append(QLatin1Char('_') + QString::number(stream)); if (miniThumb) { audioPath.append(QStringLiteral(".png")); return audioPath; } int roundedFps = (int)pCore->getCurrentFps(); audioPath.append(QStringLiteral("_%1_audio.png").arg(roundedFps)); return audioPath; } QStringList ProjectClip::updatedAnalysisData(const QString &name, const QString &data, int offset) { if (data.isEmpty()) { // Remove data return QStringList() << QString("kdenlive:clipanalysis." + name) << QString(); // m_controller->resetProperty("kdenlive:clipanalysis." + name); } QString current = getProducerProperty("kdenlive:clipanalysis." + name); if (!current.isEmpty()) { if (KMessageBox::questionYesNo(QApplication::activeWindow(), i18n("Clip already contains analysis data %1", name), QString(), KGuiItem(i18n("Merge")), KGuiItem(i18n("Add"))) == KMessageBox::Yes) { // Merge data auto &profile = pCore->getCurrentProfile(); Mlt::Geometry geometry(current.toUtf8().data(), duration().frames(profile->fps()), profile->width(), profile->height()); Mlt::Geometry newGeometry(data.toUtf8().data(), duration().frames(profile->fps()), profile->width(), profile->height()); Mlt::GeometryItem item; int pos = 0; while (newGeometry.next_key(&item, pos) == 0) { pos = item.frame(); item.frame(pos + offset); pos++; geometry.insert(item); } return QStringList() << QString("kdenlive:clipanalysis." + name) << geometry.serialise(); // m_controller->setProperty("kdenlive:clipanalysis." + name, geometry.serialise()); } // Add data with another name int i = 1; QString previous = getProducerProperty("kdenlive:clipanalysis." + name + QString::number(i)); while (!previous.isEmpty()) { ++i; previous = getProducerProperty("kdenlive:clipanalysis." + name + QString::number(i)); } return QStringList() << QString("kdenlive:clipanalysis." + name + QString::number(i)) << geometryWithOffset(data, offset); // m_controller->setProperty("kdenlive:clipanalysis." + name + QLatin1Char(' ') + QString::number(i), geometryWithOffset(data, offset)); } return QStringList() << QString("kdenlive:clipanalysis." + name) << geometryWithOffset(data, offset); // m_controller->setProperty("kdenlive:clipanalysis." + name, geometryWithOffset(data, offset)); } QMap ProjectClip::analysisData(bool withPrefix) { return getPropertiesFromPrefix(QStringLiteral("kdenlive:clipanalysis."), withPrefix); } const QString ProjectClip::geometryWithOffset(const QString &data, int offset) { if (offset == 0) { return data; } auto &profile = pCore->getCurrentProfile(); Mlt::Geometry geometry(data.toUtf8().data(), duration().frames(profile->fps()), profile->width(), profile->height()); Mlt::Geometry newgeometry(nullptr, duration().frames(profile->fps()), profile->width(), profile->height()); Mlt::GeometryItem item; int pos = 0; while (geometry.next_key(&item, pos) == 0) { pos = item.frame(); item.frame(pos + offset); pos++; newgeometry.insert(item); } return newgeometry.serialise(); } bool ProjectClip::isSplittable() const { return (m_clipType == ClipType::AV || m_clipType == ClipType::Playlist); } void ProjectClip::setBinEffectsEnabled(bool enabled) { ClipController::setBinEffectsEnabled(enabled); } void ProjectClip::registerService(std::weak_ptr timeline, int clipId, const std::shared_ptr &service, bool forceRegister) { if (!service->is_cut() || forceRegister) { int hasAudio = service->get_int("set.test_audio") == 0; int hasVideo = service->get_int("set.test_image") == 0; if (hasVideo && m_videoProducers.count(clipId) == 0) { // This is an undo producer, register it! m_videoProducers[clipId] = service; m_effectStack->addService(m_videoProducers[clipId]); } else if (hasAudio && m_audioProducers.count(clipId) == 0) { // This is an undo producer, register it! m_audioProducers[clipId] = service; m_effectStack->addService(m_audioProducers[clipId]); } } registerTimelineClip(std::move(timeline), clipId); } void ProjectClip::registerTimelineClip(std::weak_ptr timeline, int clipId) { Q_ASSERT(m_registeredClips.count(clipId) == 0); Q_ASSERT(!timeline.expired()); m_registeredClips[clipId] = std::move(timeline); setRefCount((uint)m_registeredClips.size()); } void ProjectClip::deregisterTimelineClip(int clipId) { qDebug() << " ** * DEREGISTERING TIMELINE CLIP: " << clipId; Q_ASSERT(m_registeredClips.count(clipId) > 0); m_registeredClips.erase(clipId); if (m_videoProducers.count(clipId) > 0) { m_effectStack->removeService(m_videoProducers[clipId]); m_videoProducers.erase(clipId); } if (m_audioProducers.count(clipId) > 0) { m_effectStack->removeService(m_audioProducers[clipId]); m_audioProducers.erase(clipId); } setRefCount((uint)m_registeredClips.size()); } QList ProjectClip::timelineInstances() const { QList ids; for (const auto &m_registeredClip : m_registeredClips) { ids.push_back(m_registeredClip.first); } return ids; } bool ProjectClip::selfSoftDelete(Fun &undo, Fun &redo) { auto toDelete = m_registeredClips; // we cannot use m_registeredClips directly, because it will be modified during loop for (const auto &clip : toDelete) { if (m_registeredClips.count(clip.first) == 0) { // clip already deleted, was probably grouped with another one continue; } if (auto timeline = clip.second.lock()) { timeline->requestItemDeletion(clip.first, undo, redo); } else { qDebug() << "Error while deleting clip: timeline unavailable"; Q_ASSERT(false); return false; } } return AbstractProjectItem::selfSoftDelete(undo, redo); } bool ProjectClip::isIncludedInTimeline() { return m_registeredClips.size() > 0; } void ProjectClip::replaceInTimeline() { for (const auto &clip : m_registeredClips) { if (auto timeline = clip.second.lock()) { timeline->requestClipReload(clip.first); } else { qDebug() << "Error while reloading clip: timeline unavailable"; Q_ASSERT(false); } } } void ProjectClip::updateTimelineClips(const QVector &roles) { for (const auto &clip : m_registeredClips) { if (auto timeline = clip.second.lock()) { timeline->requestClipUpdate(clip.first, roles); } else { qDebug() << "Error while reloading clip thumb: timeline unavailable"; Q_ASSERT(false); return; } } } void ProjectClip::updateZones() { int zonesCount = childCount(); if (zonesCount == 0) { resetProducerProperty(QStringLiteral("kdenlive:clipzones")); return; } QJsonArray list; for (int i = 0; i < zonesCount; ++i) { std::shared_ptr clip = std::static_pointer_cast(child(i)); if (clip) { QJsonObject currentZone; currentZone.insert(QLatin1String("name"), QJsonValue(clip->name())); QPoint zone = clip->zone(); currentZone.insert(QLatin1String("in"), QJsonValue(zone.x())); currentZone.insert(QLatin1String("out"), QJsonValue(zone.y())); if (clip->rating() > 0) { currentZone.insert(QLatin1String("rating"), QJsonValue((int)clip->rating())); } if (!clip->tags().isEmpty()) { currentZone.insert(QLatin1String("tags"), QJsonValue(clip->tags())); } list.push_back(currentZone); } } QJsonDocument json(list); setProducerProperty(QStringLiteral("kdenlive:clipzones"), QString(json.toJson())); } void ProjectClip::getThumbFromPercent(int percent) { // extract a maximum of 50 frames for bin preview percent += percent%2; int duration = getFramePlaytime(); int framePos = duration * percent / 100; if (ThumbnailCache::get()->hasThumbnail(m_binId, framePos)) { setThumbnail(ThumbnailCache::get()->getThumbnail(m_binId, framePos)); } else { // Generate percent thumbs int id; if (!pCore->jobManager()->hasPendingJob(m_binId, AbstractClipJob::CACHEJOB, &id)) { pCore->jobManager()->startJob({m_binId}, -1, QString(), 50); } } } void ProjectClip::setRating(uint rating) { AbstractProjectItem::setRating(rating); setProducerProperty(QStringLiteral("kdenlive:rating"), (int) rating); pCore->currentDoc()->setModified(true); } QVector ProjectClip::audioFrameCache(int stream) { QVector audioLevels; if (stream == -1) { if (m_audioInfo) { stream = m_audioInfo->ffmpeg_audio_index(); } else { return audioLevels; } } // convert cached image const QString cachePath = getAudioThumbPath(stream); // checking for cached thumbs QImage image(cachePath); - int channels = m_audioInfo->channels(); if (!image.isNull()) { + int channels = m_audioInfo->channels(); int n = image.width() * image.height(); for (int i = 0; i < n; i++) { QRgb p = image.pixel(i / channels, i % channels); - audioLevels << qRed(p); - audioLevels << qGreen(p); - audioLevels << qBlue(p); - audioLevels << qAlpha(p); + audioLevels << (uint8_t)qRed(p); + audioLevels << (uint8_t)qGreen(p); + audioLevels << (uint8_t)qBlue(p); + audioLevels << (uint8_t)qAlpha(p); } } return audioLevels; } void ProjectClip::setClipStatus(AbstractProjectItem::CLIPSTATUS status) { AbstractProjectItem::setClipStatus(status); if (auto ptr = m_model.lock()) { std::static_pointer_cast(ptr)->onItemUpdated(std::static_pointer_cast(shared_from_this()), AbstractProjectItem::IconOverlay); } } void ProjectClip::renameAudioStream(int id, QString name) { if (m_audioInfo) { m_audioInfo->renameStream(id, name); QString prop = QString("kdenlive:streamname.%1").arg(id); m_masterProducer->set(prop.toUtf8().constData(), name.toUtf8().constData()); if (m_audioInfo->activeStreams().keys().contains(id)) { pCore->bin()->updateTargets(clipId()); } pCore->bin()->reloadMonitorStreamIfActive(clipId()); } } diff --git a/src/jobs/audiothumbjob.cpp b/src/jobs/audiothumbjob.cpp index d83d9eee1..d5e6413ec 100644 --- a/src/jobs/audiothumbjob.cpp +++ b/src/jobs/audiothumbjob.cpp @@ -1,431 +1,431 @@ /*************************************************************************** * Copyright (C) 2017 by Nicolas Carion * * This file is part of Kdenlive. See www.kdenlive.org. * * * * This program is free software; you can redistribute it and/or modify * * it under the terms of the GNU General Public License as published by * * the Free Software Foundation; either version 2 of the License, or * * (at your option) version 3 or any later version accepted by the * * membership of KDE e.V. (or its successor approved by the membership * * of KDE e.V.), which shall act as a proxy defined in Section 14 of * * version 3 of the license. * * * * This program is distributed in the hope that it will be useful, * * but WITHOUT ANY WARRANTY; without even the implied warranty of * * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * * GNU General Public License for more details. * * * * You should have received a copy of the GNU General Public License * * along with this program. If not, see . * ***************************************************************************/ #include "audiothumbjob.hpp" #include "bin/projectclip.h" #include "bin/projectitemmodel.h" #include "core.h" #include "doc/kdenlivedoc.h" #include "doc/kthumb.h" #include "kdenlivesettings.h" #include "klocalizedstring.h" #include "lib/audio/audioStreamInfo.h" #include "macros.hpp" #include "utils/thumbnailcache.hpp" #include #include #include #include #include AudioThumbJob::AudioThumbJob(const QString &binId) : AbstractClipJob(AUDIOTHUMBJOB, binId) , m_ffmpegProcess(nullptr) { } const QString AudioThumbJob::getDescription() const { return i18n("Extracting audio thumb from clip %1", m_clipId); } bool AudioThumbJob::computeWithMlt() { m_audioLevels.clear(); m_errorMessage.clear(); // MLT audio thumbs: slower but safer QString service = m_prod->get("mlt_service"); if (service == QLatin1String("avformat-novalidate")) { service = QStringLiteral("avformat"); } else if (service.startsWith(QLatin1String("xml"))) { service = QStringLiteral("xml-nogl"); } QScopedPointer audioProducer(new Mlt::Producer(*m_prod->profile(), service.toUtf8().constData(), m_prod->get("resource"))); if (!audioProducer->is_valid()) { m_errorMessage.append(i18n("Audio thumbs: cannot open file %1", m_prod->get("resource"))); return false; } audioProducer->set("video_index", "-1"); Mlt::Filter chans(*m_prod->profile(), "audiochannels"); Mlt::Filter converter(*m_prod->profile(), "audioconvert"); Mlt::Filter levels(*m_prod->profile(), "audiolevel"); audioProducer->attach(chans); audioProducer->attach(converter); audioProducer->attach(levels); int last_val = 0; double framesPerSecond = audioProducer->get_fps(); mlt_audio_format audioFormat = mlt_audio_s16; QStringList keys; keys.reserve(m_channels); for (int i = 0; i < m_channels; i++) { keys << "meta.media.audio_level." + QString::number(i); } double maxLevel = 1; QVector mltLevels; for (int z = 0; z < m_lengthInFrames; ++z) { int val = (int)(100.0 * z / m_lengthInFrames); if (last_val != val) { emit jobProgress(val); last_val = val; } QScopedPointer mltFrame(audioProducer->get_frame()); if ((mltFrame != nullptr) && mltFrame->is_valid() && (mltFrame->get_int("test_audio") == 0)) { int samples = mlt_sample_calculator(float(framesPerSecond), m_frequency, z); mltFrame->get_audio(audioFormat, m_frequency, m_channels, samples); for (int channel = 0; channel < m_channels; ++channel) { double lev = mltFrame->get_double(keys.at(channel).toUtf8().constData()); mltLevels << lev; maxLevel = qMax(lev, maxLevel); } } else if (!mltLevels.isEmpty()) { for (int channel = 0; channel < m_channels; channel++) { mltLevels << mltLevels.last(); } } } // Normalize for (double &v : mltLevels) { m_audioLevels << 255 * v / maxLevel; } m_done = true; return true; } bool AudioThumbJob::computeWithFFMPEG() { QString filePath = m_prod->get("kdenlive:originalurl"); if (filePath.isEmpty()) { filePath = m_prod->get("resource"); } m_ffmpegProcess.reset(new QProcess); QString thumbPath = m_binClip->getAudioThumbPath(m_audioStream, true); int audioStreamIndex = m_binClip->getAudioStreamFfmpegIndex(m_audioStream); if (!QFile::exists(thumbPath)) { // Generate thumbnail used in monitor overlay QStringList args; args << QStringLiteral("-hide_banner") << QStringLiteral("-y")<< QStringLiteral("-i") << QUrl::fromLocalFile(filePath).toLocalFile() << QString("-filter_complex"); if (m_audioStream >= 0) { args << QString("[a:%1]showwavespic=s=%2x%3:split_channels=1:scale=cbrt:colors=0xffdddd|0xddffdd").arg(audioStreamIndex).arg(m_thumbSize.width()).arg(m_thumbSize.height()); } else { // Only 1 audio stream in clip args << QString("[a]showwavespic=s=%2x%3:split_channels=1:scale=cbrt:colors=0xffdddd|0xddffdd").arg(m_thumbSize.width()).arg(m_thumbSize.height()); } args << QStringLiteral("-frames:v") << QStringLiteral("1"); args << thumbPath; qDebug()<<"=== FFARGS: "<kill(); } m_done = true; m_successful = false; }); m_ffmpegProcess->start(KdenliveSettings::ffmpegpath(), args); m_ffmpegProcess->waitForFinished(-1); disconnect(m_ffmpegProcess.get(), &QProcess::readyReadStandardOutput, this, &AudioThumbJob::updateFfmpegProgress); if (m_ffmpegProcess->exitStatus() != QProcess::CrashExit) { if (m_dataInCache || !KdenliveSettings::audiothumbnails()) { m_done = true; return true; } } } if (!m_dataInCache && !m_done && KdenliveSettings::audiothumbnails()) { // Generate timeline audio thumbnail data m_audioLevels.clear(); std::vector> channelFiles; for (int i = 0; i < m_channels; i++) { std::unique_ptr channelTmpfile(new QTemporaryFile()); if (!channelTmpfile->open()) { m_errorMessage.append(i18n("Audio thumbs: cannot create temporary file, check disk space and permissions\n")); return false; } channelTmpfile->close(); channelFiles.emplace_back(std::move(channelTmpfile)); } // Always create audio thumbs from the original source file, because proxy // can have a different audio config (channels / mono/ stereo) QStringList args {QStringLiteral("-hide_banner"), QStringLiteral("-i"), QUrl::fromLocalFile(filePath).toLocalFile(), QStringLiteral("-progress")}; #ifdef Q_OS_WIN args << QStringLiteral("-"); #else args << QStringLiteral("/dev/stdout"); #endif bool isFFmpeg = KdenliveSettings::ffmpegpath().contains(QLatin1String("ffmpeg")); args << QStringLiteral("-filter_complex"); if (m_channels == 1) { if (m_audioStream >= 0) { args << QStringLiteral("[a:%1]aformat=channel_layouts=mono,%2=100").arg(audioStreamIndex).arg(isFFmpeg ? "aresample=async" : "sample_rates"); } else { args << QStringLiteral("[a]aformat=channel_layouts=mono,%1=100").arg(isFFmpeg ? "aresample=async" : "sample_rates"); } /*args << QStringLiteral("-map") << QStringLiteral("0:a%1").arg(m_audioStream > 0 ? ":" + QString::number(audioStreamIndex) : QString())*/ args << QStringLiteral("-c:a") << QStringLiteral("pcm_s16le") << QStringLiteral("-frames:v") << QStringLiteral("1") << QStringLiteral("-y") << QStringLiteral("-f") << QStringLiteral("data") << channelFiles[0]->fileName(); } else { QString aformat = QStringLiteral("[a%1]%2=100,channelsplit=channel_layout=%3") .arg(audioStreamIndex >= 0 ? ":" + QString::number(audioStreamIndex) : QString()) .arg(isFFmpeg ? "aresample=async" : "aformat=sample_rates") .arg(m_channels > 2 ? "5.1" : "stereo"); for (int i = 0; i < m_channels; ++i) { aformat.append(QStringLiteral("[0:%1]").arg(i)); } args << aformat; args << QStringLiteral("-frames:v") << QStringLiteral("1"); for (int i = 0; i < m_channels; i++) { // Channel 1 args << QStringLiteral("-map") << QStringLiteral("[0:%1]").arg(i) << QStringLiteral("-c:a") << QStringLiteral("pcm_s16le") << QStringLiteral("-y") << QStringLiteral("-f") << QStringLiteral("data") << channelFiles[size_t(i)]->fileName(); } } m_ffmpegProcess.reset(new QProcess); connect(m_ffmpegProcess.get(), &QProcess::readyReadStandardOutput, this, &AudioThumbJob::updateFfmpegProgress, Qt::UniqueConnection); connect(this, &AudioThumbJob::jobCanceled, [&]() { if (m_ffmpegProcess) { disconnect(m_ffmpegProcess.get(), &QProcess::readyReadStandardOutput, this, &AudioThumbJob::updateFfmpegProgress); m_ffmpegProcess->kill(); } }); m_ffmpegProcess->start(KdenliveSettings::ffmpegpath(), args); m_ffmpegProcess->waitForFinished(-1); disconnect(m_ffmpegProcess.get(), &QProcess::readyReadStandardOutput, this, &AudioThumbJob::updateFfmpegProgress); if (m_ffmpegProcess->exitStatus() != QProcess::CrashExit) { int dataSize = 0; std::vector rawChannels; std::vector sourceChannels; for (auto &channelFile : channelFiles) { channelFile->open(); sourceChannels.emplace_back(channelFile->readAll()); QByteArray &res = sourceChannels.back(); channelFile->close(); if (dataSize == 0) { dataSize = res.size(); } if (res.isEmpty() || res.size() != dataSize) { // Something went wrong, abort m_errorMessage.append(i18n("Audio thumbs: error reading audio thumbnail created with FFmpeg\n")); return false; } rawChannels.emplace_back(reinterpret_cast(res.constData())); } int progress = 0; std::vector channelsData; double offset = (double)dataSize / (2.0 * m_lengthInFrames); int intraOffset = 1; if (offset > 1000) { intraOffset = offset / 60; } else if (offset > 250) { intraOffset = offset / 10; } long maxLevel = 1; QVector ffmpegLevels; for (int i = 0; i < m_lengthInFrames; i++) { channelsData.resize((size_t)rawChannels.size()); std::fill(channelsData.begin(), channelsData.end(), 0); int pos = (int)(i * offset); int steps = 0; for (int j = 0; j < (int)offset && (pos + j < dataSize); j += intraOffset) { steps++; for (size_t k = 0; k < rawChannels.size(); k++) { channelsData[k] += abs(rawChannels[k][pos + j]); } } for (long &k : channelsData) { if (steps != 0) { k /= steps; } maxLevel = qMax(k, maxLevel); ffmpegLevels << k; } int p = 80 + (i * 20 / m_lengthInFrames); if (p != progress) { emit jobProgress(p); progress = p; } } for (long &v : ffmpegLevels) { m_audioLevels << (uint8_t) (255 * v / maxLevel); } m_done = true; return true; } } if (!KdenliveSettings::audiothumbnails()) { // We only wanted the thumb generation return true; } QString err = m_ffmpegProcess->readAllStandardError(); m_ffmpegProcess.reset(); // m_errorMessage += err; // m_errorMessage.append(i18n("Failed to create FFmpeg audio thumbnails, we now try to use MLT")); qWarning() << "Failed to create FFmpeg audio thumbs:\n" << err << "\n---------------------"; return false; } void AudioThumbJob::updateFfmpegProgress() { if (m_ffmpegProcess == nullptr) { return; } QString result = m_ffmpegProcess->readAllStandardOutput(); const QStringList lines = result.split(QLatin1Char('\n')); for (const QString &data : lines) { if (data.startsWith(QStringLiteral("out_time_ms"))) { double ms = data.section(QLatin1Char('='), 1).toDouble(); emit jobProgress((int)(ms / m_binClip->duration().ms() / 10)); } else { m_logDetails += data + QStringLiteral("\n"); } } } bool AudioThumbJob::startJob() { if (m_done) { return true; } m_dataInCache = false; m_binClip = pCore->projectItemModel()->getClipByBinID(m_clipId); if (m_binClip == nullptr) { // Clip was deleted return false; } if (m_binClip->audioChannels() == 0 || m_binClip->audioThumbCreated()) { // nothing to do m_done = true; m_successful = true; return true; } m_thumbSize = QSize(1000, 1000 / pCore->getCurrentDar()); m_prod = m_binClip->originalProducer(); m_frequency = m_binClip->audioInfo()->samplingRate(); m_frequency = m_frequency <= 0 ? 48000 : m_frequency; m_channels = m_binClip->audioInfo()->channels(); m_channels = m_channels <= 0 ? 2 : m_channels; - m_lengthInFrames = m_prod->get_length(); + m_lengthInFrames = m_prod->get_length(); // Multiply this if we want more than 1 sample per frame QMap streams = m_binClip->audioInfo()->streams(); if ((m_prod == nullptr) || !m_prod->is_valid()) { m_errorMessage.append(i18n("Audio thumbs: cannot open project file %1", m_binClip->url())); m_done = true; m_successful = false; return false; } QMapIterator st(streams); while (st.hasNext()) { st.next(); int stream = st.key(); // Generate one thumb per stream m_audioStream = stream; m_cachePath = m_binClip->getAudioThumbPath(stream); // checking for cached thumbs QImage image(m_cachePath); if (!image.isNull()) { // Audio cache already exists continue; } m_done = false; bool ok = m_binClip->clipType() == ClipType::Playlist ? (KdenliveSettings::audiothumbnails() ? false : true) : computeWithFFMPEG(); ok = ok ? ok : computeWithMlt(); Q_ASSERT(ok == m_done); if (ok && m_done && !m_audioLevels.isEmpty()) { // Put into an image for caching. int count = m_audioLevels.size(); image = QImage((int)lrint((count + 3) / 4.0 / m_channels), m_channels, QImage::Format_ARGB32); int n = image.width() * image.height(); for (int i = 0; i < n; i++) { QRgb p; if ((4 * i + 3) < count) { p = qRgba(m_audioLevels.at(4 * i), m_audioLevels.at(4 * i + 1), m_audioLevels.at(4 * i + 2), m_audioLevels.at(4 * i + 3)); } else { int last = m_audioLevels.last(); int r = (4 * i + 0) < count ? m_audioLevels.at(4 * i + 0) : last; int g = (4 * i + 1) < count ? m_audioLevels.at(4 * i + 1) : last; int b = (4 * i + 2) < count ? m_audioLevels.at(4 * i + 2) : last; int a = last; p = qRgba(r, g, b, a); } image.setPixel(i / m_channels, i % m_channels, p); } image.save(m_cachePath); } m_audioLevels.clear(); } if (m_done || !KdenliveSettings::audiothumbnails()) { m_successful = true; return true; } m_done = true; m_successful = false; return false; } bool AudioThumbJob::commitResult(Fun &undo, Fun &redo) { Q_ASSERT(!m_resultConsumed); m_ffmpegProcess.reset(); if (!m_done) { qDebug() << "ERROR: Trying to consume invalid results"; return false; } m_resultConsumed = true; if (!m_successful) { return false; } QImage oldImage; QImage result; if (m_binClip->clipType() == ClipType::Audio) { oldImage = m_binClip->thumbnail(m_thumbSize.width(), m_thumbSize.height()).toImage(); result = ThumbnailCache::get()->getAudioThumbnail(m_clipId); } // note that the image is moved into lambda, it won't be available from this class anymore auto operation = [clip = m_binClip, image = std::move(result)]() { clip->updateAudioThumbnail(); if (!image.isNull() && clip->clipType() == ClipType::Audio) { clip->setThumbnail(image); } return true; }; auto reverse = [clip = m_binClip, image = std::move(oldImage)]() { clip->updateAudioThumbnail(); if (!image.isNull() && clip->clipType() == ClipType::Audio) { clip->setThumbnail(image); } return true; }; bool ok = operation(); if (ok) { UPDATE_UNDO_REDO_NOLOCK(operation, reverse, undo, redo); } return ok; } diff --git a/src/timeline2/view/qml/ClipAudioThumbs.qml b/src/timeline2/view/qml/ClipAudioThumbs.qml index 7b75ba9bb..0ce100c8e 100644 --- a/src/timeline2/view/qml/ClipAudioThumbs.qml +++ b/src/timeline2/view/qml/ClipAudioThumbs.qml @@ -1,57 +1,59 @@ import QtQuick 2.11 import QtQuick.Controls 2.4 import Kdenlive.Controls 1.0 import QtQml.Models 2.11 import com.enums 1.0 Row { id: waveform opacity: clipStatus == ClipState.Disabled ? 0.2 : 1 property int maxWidth: 500 + 100 * timeline.scaleFactor anchors.fill: parent Timer { id: waveTimer interval: 50; running: false; repeat: false onTriggered: processReload() } function reload(reset) { if (reset == 0) { waveformRepeater.model = 0 } waveTimer.start() } onMaxWidthChanged: { waveTimer.start(); } function processReload() { // This is needed to make the model have the correct count. // Model as a property expression is not working in all cases. if (!waveform.visible || !timeline.showAudioThumbnails) { return; } var chunks = Math.ceil(waveform.width / waveform.maxWidth) if (waveformRepeater.model == undefined || chunks != waveformRepeater.model) { waveformRepeater.model = chunks } } Repeater { id: waveformRepeater TimelineWaveform { width: Math.min(waveform.width, waveform.maxWidth) height: waveform.height channels: clipRoot.audioChannels binId: clipRoot.binId audioStream: Math.abs(clipRoot.audioStream) isFirstChunk: index == 0 - showItem: waveform.visible && (index * waveform.maxWidth < (clipRoot.scrollStart + scrollView.contentItem.width)) && ((index * waveform.maxWidth + width) > clipRoot.scrollStart) + showItem: waveform.visible && (index * waveform.maxWidth < (clipRoot.scrollStart + scrollView.width)) && ((index * waveform.maxWidth + width) > clipRoot.scrollStart) format: timeline.audioThumbFormat + drawInPoint: Math.max(0, clipRoot.scrollStart - (index * waveform.maxWidth)) + drawOutPoint: (clipRoot.scrollStart + scrollView.width - (index * waveform.maxWidth)) waveInPoint: clipRoot.speed < 0 ? (Math.round(clipRoot.outPoint - (index * waveform.maxWidth / clipRoot.timeScale) * Math.abs(clipRoot.speed)) * channels) : (Math.round((clipRoot.inPoint + (index * waveform.maxWidth / clipRoot.timeScale)) * clipRoot.speed) * channels) waveOutPoint: clipRoot.speed < 0 ? (waveInPoint - Math.ceil(width / clipRoot.timeScale * Math.abs(clipRoot.speed)) * channels) : (waveInPoint + Math.round(width / clipRoot.timeScale * clipRoot.speed) * channels) fillColor: activePalette.text } } } diff --git a/src/timeline2/view/qml/ClipThumbs.qml b/src/timeline2/view/qml/ClipThumbs.qml index 1cafc1506..417acc410 100644 --- a/src/timeline2/view/qml/ClipThumbs.qml +++ b/src/timeline2/view/qml/ClipThumbs.qml @@ -1,80 +1,80 @@ import QtQuick 2.11 import QtQuick.Controls 2.4 import QtQml.Models 2.11 import com.enums 1.0 Row { id: thumbRow anchors.fill: parent visible: !isAudio opacity: clipStatus == ClipState.Disabled ? 0.2 : 1 property bool fixedThumbs: clipRoot.itemType == ProducerType.Image || clipRoot.itemType == ProducerType.Text || clipRoot.itemType == ProducerType.TextTemplate property int thumbWidth: container.height * root.dar property bool enableCache: clipRoot.itemType == ProducerType.Video || clipRoot.itemType == ProducerType.AV function reload(reset) { //console.log('+++++\n\ntriggered ML thumb reload\n\n++++++++++++++') clipRoot.baseThumbPath = clipRoot.variableThumbs ? '' : 'image://thumbnail/' + clipRoot.binId + '/' + Math.random() + '/#' } Repeater { id: thumbRepeater // switching the model allows one to have different view modes: // 2: will display start / end thumbs // container.width / thumbRow.thumbWidth will display all frames showThumbnails // 1: only show first thumbnail // 0: will disable thumbnails model: parentTrack.trackThumbsFormat == 0 ? 2 : parentTrack.trackThumbsFormat == 1 ? Math.ceil(container.width / thumbRow.thumbWidth) : parentTrack.trackThumbsFormat == 2 ? 1 : 0 property int startFrame: clipRoot.inPoint property int endFrame: clipRoot.outPoint property real imageWidth: Math.max(thumbRow.thumbWidth, container.width / thumbRepeater.count) property int thumbStartFrame: fixedThumbs ? 0 : (clipRoot.speed >= 0) ? Math.round(clipRoot.inPoint * clipRoot.speed) : Math.round((clipRoot.maxDuration - clipRoot.inPoint) * -clipRoot.speed - 1) property int thumbEndFrame: fixedThumbs ? 0 : (clipRoot.speed >= 0) ? Math.round(clipRoot.outPoint * clipRoot.speed) : Math.round((clipRoot.maxDuration - clipRoot.outPoint) * -clipRoot.speed - 1) Image { width: thumbRepeater.imageWidth height: container.height fillMode: Image.PreserveAspectFit asynchronous: true cache: enableCache property int currentFrame: fixedThumbs ? 0 : thumbRepeater.count < 3 ? (index == 0 ? thumbRepeater.thumbStartFrame : thumbRepeater.thumbEndFrame) : Math.floor(clipRoot.inPoint + Math.round((index) * width / timeline.scaleFactor)* clipRoot.speed) horizontalAlignment: thumbRepeater.count < 3 ? (index == 0 ? Image.AlignLeft : Image.AlignRight) : Image.AlignLeft - source: thumbRepeater.count < 3 ? (clipRoot.baseThumbPath + currentFrame) : (index * width < clipRoot.scrollStart - width || index * width > clipRoot.scrollStart + scrollView.contentItem.width) ? '' : clipRoot.baseThumbPath + currentFrame + source: thumbRepeater.count < 3 ? (clipRoot.baseThumbPath + currentFrame) : (index * width < clipRoot.scrollStart - width || index * width > clipRoot.scrollStart + scrollView.width) ? '' : clipRoot.baseThumbPath + currentFrame onStatusChanged: { if (thumbRepeater.count < 3) { if (status === Image.Ready) { thumbPlaceholder.source = source } } } BusyIndicator { running: parent.status != Image.Ready anchors.left: parent.left anchors.leftMargin: index < thumbRepeater.count - 1 ? 0 : parent.width - thumbRow.thumbWidth - 1 implicitWidth: thumbRepeater.imageWidth implicitHeight: container.height hoverEnabled: false visible: running contentItem: Image { id: thumbPlaceholder visible: parent.running anchors.fill: parent horizontalAlignment: Image.AlignLeft fillMode: Image.PreserveAspectFit asynchronous: true } } Rectangle { visible: thumbRepeater.count < 3 anchors.left: parent.left anchors.leftMargin: index == 0 ? thumbRow.thumbWidth : parent.width - thumbRow.thumbWidth - 1 color: "#ffffff" opacity: 0.3 width: 1 height: parent.height } } } } diff --git a/src/timeline2/view/qml/Track.qml b/src/timeline2/view/qml/Track.qml index 3e10a9d9e..dbb58eef1 100644 --- a/src/timeline2/view/qml/Track.qml +++ b/src/timeline2/view/qml/Track.qml @@ -1,433 +1,433 @@ /* * Copyright (c) 2013-2016 Meltytech, LLC * Author: Dan Dennedy * * 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 . */ import QtQuick 2.11 import QtQml.Models 2.11 import com.enums 1.0 Item{ id: trackRoot property alias trackModel: trackModel.model property alias rootIndex : trackModel.rootIndex property bool isAudio property real timeScale: 1.0 property bool isCurrentTrack: false property bool isLocked: false property int trackInternalId : -42 property int trackThumbsFormat property int itemType: 0 opacity: model.disabled ? 0.4 : 1 function clipAt(index) { return repeater.itemAt(index) } function isClip(type) { return type != ProducerType.Composition && type != ProducerType.Track; } width: clipRow.width DelegateModel { id: trackModel delegate: Item { property var itemModel : model z: model.clipType == ProducerType.Composition ? 5 : 0 Loader { id: loader Binding { target: loader.item property: "timeScale" value: trackRoot.timeScale when: loader.status == Loader.Ready && loader.item } Binding { target: loader.item property: "fakeTid" value: model.fakeTrackId when: loader.status == Loader.Ready && loader.item && isClip(model.clipType) } Binding { target: loader.item property: "fakePosition" value: model.fakePosition when: loader.status == Loader.Ready && loader.item && isClip(model.clipType) } Binding { target: loader.item property: "selected" value: model.selected when: loader.status == Loader.Ready && model.clipType != ProducerType.Track } Binding { target: loader.item property: "mltService" value: model.mlt_service when: loader.status == Loader.Ready && loader.item } Binding { target: loader.item property: "modelStart" value: model.start when: loader.status == Loader.Ready && loader.item } Binding { target: loader.item property: "scrollX" value: scrollView.contentX when: loader.status == Loader.Ready && loader.item } Binding { target: loader.item property: "fadeIn" value: model.fadeIn when: loader.status == Loader.Ready && isClip(model.clipType) } Binding { target: loader.item property: "positionOffset" value: model.positionOffset when: loader.status == Loader.Ready && isClip(model.clipType) } Binding { target: loader.item property: "effectNames" value: model.effectNames when: loader.status == Loader.Ready && isClip(model.clipType) } Binding { target: loader.item property: "clipStatus" value: model.clipStatus when: loader.status == Loader.Ready && isClip(model.clipType) } Binding { target: loader.item property: "fadeOut" value: model.fadeOut when: loader.status == Loader.Ready && isClip(model.clipType) } Binding { target: loader.item property: "showKeyframes" value: model.showKeyframes when: loader.status == Loader.Ready && loader.item } Binding { target: loader.item property: "isGrabbed" value: model.isGrabbed when: loader.status == Loader.Ready && loader.item } Binding { target: loader.item property: "keyframeModel" value: model.keyframeModel when: loader.status == Loader.Ready && loader.item } Binding { target: loader.item property: "aTrack" value: model.a_track when: loader.status == Loader.Ready && model.clipType == ProducerType.Composition } Binding { target: loader.item property: "trackHeight" value: root.trackHeight when: loader.status == Loader.Ready && model.clipType == ProducerType.Composition } Binding { target: loader.item property: "clipDuration" value: model.duration when: loader.status == Loader.Ready && loader.item } Binding { target: loader.item property: "inPoint" value: model.in when: loader.status == Loader.Ready && loader.item } Binding { target: loader.item property: "outPoint" value: model.out when: loader.status == Loader.Ready && loader.item } Binding { target: loader.item property: "grouped" value: model.grouped when: loader.status == Loader.Ready && loader.item } Binding { target: loader.item property: "clipName" value: model.name when: loader.status == Loader.Ready && loader.item } Binding { target: loader.item property: "clipResource" value: model.resource when: loader.status == Loader.Ready && isClip(model.clipType) } Binding { target: loader.item property: "isProxy" value: model.isProxy when: loader.status == Loader.Ready && isClip(model.clipType) } Binding { target: loader.item property: "maxDuration" value: model.maxDuration when: loader.status == Loader.Ready && isClip(model.clipType) } Binding { target: loader.item property: "forceReloadThumb" value: model.reloadThumb when: loader.status == Loader.Ready && isClip(model.clipType) } Binding { target: loader.item property: "binId" value: model.binId when: loader.status == Loader.Ready && isClip(model.clipType) } sourceComponent: { if (isClip(model.clipType)) { return clipDelegate } else if (model.clipType == ProducerType.Composition) { return compositionDelegate } else { // Track return undefined } } onLoaded: { item.clipId= model.item item.parentTrack = trackRoot if (isClip(model.clipType)) { console.log('loaded clip: ', model.start, ', ID: ', model.item, ', index: ', trackRoot.DelegateModel.itemsIndex,', TYPE:', model.clipType) item.isAudio= model.audio item.markers= model.markers item.hasAudio = model.hasAudio item.canBeAudio = model.canBeAudio item.canBeVideo = model.canBeVideo item.itemType = model.clipType item.audioChannels = model.audioChannels item.audioStream = model.audioStream item.aStreamIndex = model.audioStreamIndex - console.log('loaded clpi with Astream: ', model.audioStream) + console.log('loaded clip with Astream: ', model.audioStream) // Speed change triggers a new clip insert so no binding necessary item.speed = model.speed } else if (model.clipType == ProducerType.Composition) { console.log('loaded composition: ', model.start, ', ID: ', model.item, ', index: ', trackRoot.DelegateModel.itemsIndex) //item.aTrack = model.a_track } else { console.log('loaded unwanted element: ', model.item, ', index: ', trackRoot.DelegateModel.itemsIndex) } item.trackId = model.trackId //item.selected= trackRoot.selection.indexOf(item.clipId) !== -1 //console.log(width, height); } } } } Item { id: clipRow height: trackRoot.height Repeater { id: repeater; model: trackModel } } Component { id: clipDelegate Clip { height: trackRoot.height onInitGroupTrim: { // We are resizing a group, remember coordinates of all elements clip.groupTrimData = controller.getGroupData(clip.clipId) } onTrimmingIn: { if (controlTrim) { newDuration = controller.requestItemSpeedChange(clip.clipId, newDuration, false, root.snapping) if (!speedController.visible) { // Store original speed speedController.originalSpeed = clip.speed } clip.x += clip.width - (newDuration * trackRoot.timeScale) clip.width = newDuration * root.timeScale speedController.x = clip.x + clip.border.width speedController.width = clip.width - 2 * clip.border.width speedController.lastValidDuration = newDuration clip.speed = clip.originalDuration * speedController.originalSpeed / newDuration speedController.visible = true return } var new_duration = controller.requestItemResize(clip.clipId, newDuration, false, false, root.snapping, shiftTrim) if (new_duration > 0) { clip.lastValidDuration = new_duration clip.originalX = clip.draggedX // Show amount trimmed as a time in a "bubble" help. var delta = new_duration - clip.originalDuration var s = timeline.simplifiedTC(Math.abs(delta)) s = '%1%2\n%3:%4'.arg((delta <= 0)? '+' : '-') .arg(s) .arg(i18n("In")) .arg(timeline.simplifiedTC(clip.inPoint)) bubbleHelp.show(clip.x - 20, trackRoot.y + trackRoot.height, s) } } onTrimmedIn: { bubbleHelp.hide() if (shiftTrim || clip.groupTrimData == undefined || controlTrim) { // We only resize one element controller.requestItemResize(clip.clipId, clip.originalDuration, false, false, 0, shiftTrim) if (controlTrim) { // Update speed speedController.visible = false controller.requestClipResizeAndTimeWarp(clip.clipId, speedController.lastValidDuration, false, root.snapping, shiftTrim, clip.originalDuration * speedController.originalSpeed / speedController.lastValidDuration) speedController.originalSpeed = 1 } else { controller.requestItemResize(clip.clipId, clip.lastValidDuration, false, true, 0, shiftTrim) } } else { var updatedGroupData = controller.getGroupData(clip.clipId) controller.processGroupResize(clip.groupTrimData, updatedGroupData, false) } clip.groupTrimData = undefined } onTrimmingOut: { if (controlTrim) { if (!speedController.visible) { // Store original speed speedController.originalSpeed = clip.speed } speedController.x = clip.x + clip.border.width newDuration = controller.requestItemSpeedChange(clip.clipId, newDuration, true, root.snapping) clip.width = newDuration * trackRoot.timeScale speedController.width = clip.width - 2 * clip.border.width speedController.lastValidDuration = newDuration clip.speed = clip.originalDuration * speedController.originalSpeed / newDuration speedController.visible = true return } var new_duration = controller.requestItemResize(clip.clipId, newDuration, true, false, root.snapping, shiftTrim) if (new_duration > 0) { clip.lastValidDuration = new_duration // Show amount trimmed as a time in a "bubble" help. var delta = clip.originalDuration - new_duration var s = timeline.simplifiedTC(Math.abs(delta)) s = '%1%2\n%3:%4'.arg((delta <= 0)? '+' : '-') .arg(s) .arg(i18n("Duration")) .arg(timeline.simplifiedTC(new_duration)) bubbleHelp.show(clip.x + clip.width - 20, trackRoot.y + trackRoot.height, s) } } onTrimmedOut: { bubbleHelp.hide() if (shiftTrim || clip.groupTrimData == undefined || controlTrim) { controller.requestItemResize(clip.clipId, clip.originalDuration, true, false, 0, shiftTrim) if (controlTrim) { speedController.visible = false // Update speed controller.requestClipResizeAndTimeWarp(clip.clipId, speedController.lastValidDuration, true, root.snapping, shiftTrim, clip.originalDuration * speedController.originalSpeed / speedController.lastValidDuration) speedController.originalSpeed = 1 } else { controller.requestItemResize(clip.clipId, clip.lastValidDuration, true, true, 0, shiftTrim) } } else { var updatedGroupData = controller.getGroupData(clip.clipId) controller.processGroupResize(clip.groupTrimData, updatedGroupData, true) } clip.groupTrimData = undefined } } } Component { id: compositionDelegate Composition { displayHeight: Math.max(trackRoot.height / 2, trackRoot.height - (root.baseUnit * 2)) opacity: 0.8 selected: root.timelineSelection.indexOf(clipId) !== -1 onTrimmingIn: { var new_duration = controller.requestItemResize(clip.clipId, newDuration, false, false, root.snapping) if (new_duration > 0) { clip.lastValidDuration = newDuration clip.originalX = clip.draggedX // Show amount trimmed as a time in a "bubble" help. var delta = clip.originalDuration - new_duration var s = timeline.simplifiedTC(Math.abs(delta)) s = '%1%2 = %3'.arg((delta <= 0)? '+' : '-') .arg(s) .arg(timeline.simplifiedTC(new_duration)) bubbleHelp.show(clip.x + clip.width, trackRoot.y + trackRoot.height, s) } } onTrimmedIn: { bubbleHelp.hide() controller.requestItemResize(clip.clipId, clip.originalDuration, false, false, root.snapping) controller.requestItemResize(clip.clipId, clip.lastValidDuration, false, true, root.snapping) } onTrimmingOut: { var new_duration = controller.requestItemResize(clip.clipId, newDuration, true, false, root.snapping) if (new_duration > 0) { clip.lastValidDuration = newDuration // Show amount trimmed as a time in a "bubble" help. var delta = clip.originalDuration - new_duration var s = timeline.simplifiedTC(Math.abs(delta)) s = '%1%2 = %3'.arg((delta <= 0)? '+' : '-') .arg(s) .arg(timeline.simplifiedTC(new_duration)) bubbleHelp.show(clip.x + clip.width, trackRoot.y + trackRoot.height, s) } } onTrimmedOut: { bubbleHelp.hide() controller.requestItemResize(clip.clipId, clip.originalDuration, true, false, root.snapping) controller.requestItemResize(clip.clipId, clip.lastValidDuration, true, true, root.snapping) } } } Rectangle { id: speedController anchors.bottom: parent.bottom color: activePalette.highlight //'#cccc0000' visible: false height: root.baseUnit * 1.5 property int lastValidDuration: 0 property real originalSpeed: 1 Text { id: speedLabel text: i18n("Adjusting speed") font: miniFont anchors.fill: parent verticalAlignment: Text.AlignVCenter horizontalAlignment: Text.AlignHCenter color: activePalette.highlightedText } transitions: [ Transition { NumberAnimation { property: "opacity"; duration: 300} } ] } } diff --git a/src/timeline2/view/qml/timelineitems.cpp b/src/timeline2/view/qml/timelineitems.cpp index 314a5460d..62d5f9ffa 100644 --- a/src/timeline2/view/qml/timelineitems.cpp +++ b/src/timeline2/view/qml/timelineitems.cpp @@ -1,243 +1,284 @@ /* Based on Shotcut, Copyright (c) 2015-2016 Meltytech, LLC Copyright (C) 2019 Jean-Baptiste Mardelle This file is part of Kdenlive. See www.kdenlive.org. This program is free software; you can redistribute it and/or modify it under the terms of the GNU General Public License as published by the Free Software Foundation; either version 2 of the License or (at your option) version 3 or any later version accepted by the membership of KDE e.V. (or its successor approved by the membership of KDE e.V.), which shall act as a proxy defined in Section 14 of version 3 of the license. This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for more details. You should have received a copy of the GNU General Public License along with this program. If not, see . */ #include "kdenlivesettings.h" #include "core.h" #include "bin/projectitemmodel.h" #include #include #include +#include #include const QStringList chanelNames{"L", "R", "C", "LFE", "BL", "BR"}; class TimelineTriangle : public QQuickPaintedItem { Q_OBJECT Q_PROPERTY(QColor fillColor MEMBER m_color) public: TimelineTriangle() { setAntialiasing(true); } void paint(QPainter *painter) override { QPainterPath path; path.moveTo(0, 0); path.lineTo(width(), 0); path.lineTo(0, height()); painter->fillPath(path, m_color); painter->setPen(Qt::white); painter->drawLine(width(), 0, 0, height()); } private: QColor m_color; }; class TimelinePlayhead : public QQuickPaintedItem { Q_OBJECT Q_PROPERTY(QColor fillColor MEMBER m_color NOTIFY colorChanged) public: TimelinePlayhead() { connect(this, SIGNAL(colorChanged(QColor)), this, SLOT(update())); } void paint(QPainter *painter) override { QPainterPath path; path.moveTo(width(), 0); path.lineTo(width() / 2.0, height()); path.lineTo(0, 0); painter->fillPath(path, m_color); } signals: void colorChanged(const QColor &); private: QColor m_color; }; class TimelineWaveform : public QQuickPaintedItem { Q_OBJECT Q_PROPERTY(QColor fillColor MEMBER m_color NOTIFY propertyChanged) Q_PROPERTY(int waveInPoint MEMBER m_inPoint NOTIFY propertyChanged) + Q_PROPERTY(int drawInPoint MEMBER m_drawInPoint NOTIFY propertyChanged) + Q_PROPERTY(int drawOutPoint MEMBER m_drawOutPoint NOTIFY propertyChanged) Q_PROPERTY(int channels MEMBER m_channels NOTIFY audioChannelsChanged) Q_PROPERTY(QString binId MEMBER m_binId NOTIFY levelsChanged) Q_PROPERTY(int waveOutPoint MEMBER m_outPoint) Q_PROPERTY(int audioStream MEMBER m_stream) Q_PROPERTY(bool format MEMBER m_format NOTIFY propertyChanged) Q_PROPERTY(bool showItem READ showItem WRITE setShowItem NOTIFY showItemChanged) Q_PROPERTY(bool isFirstChunk MEMBER m_firstChunk) public: TimelineWaveform() { setAntialiasing(false); // setClip(true); setEnabled(false); m_showItem = false; + m_precisionFactor = 1; //setRenderTarget(QQuickPaintedItem::FramebufferObject); //setMipmap(true); setTextureSize(QSize(1, 1)); connect(this, &TimelineWaveform::levelsChanged, [&]() { if (!m_binId.isEmpty() && m_audioLevels.isEmpty() && m_stream >= 0) { - m_audioLevels = pCore->projectItemModel()->getAudioLevelsByBinID(m_binId, m_stream); update(); } }); connect(this, &TimelineWaveform::propertyChanged, [&]() { update(); }); } - bool showItem() const { return m_showItem; } void setShowItem(bool show) { m_showItem = show; if (show) { setTextureSize(QSize(width(), height())); update(); } else { // Free memory setTextureSize(QSize(1, 1)); } } void paint(QPainter *painter) override { if (!m_showItem || m_binId.isEmpty()) { return; } if (m_audioLevels.isEmpty() && m_stream >= 0) { m_audioLevels = pCore->projectItemModel()->getAudioLevelsByBinID(m_binId, m_stream); if (m_audioLevels.isEmpty()) { return; } } - qreal indicesPrPixel = qreal(m_outPoint - m_inPoint) / width(); + qreal indicesPrPixel = qreal(m_outPoint - m_inPoint) / width() * m_precisionFactor; + //qDebug()<<"== GOT DIMENSIONS FOR WAVE: "<<(m_outPoint - m_inPoint)<<", WID: "<pen(); pen.setColor(m_color); - pen.setWidthF(0); painter->setBrush(m_color); + pen.setCapStyle(Qt::FlatCap); + double increment = qMax(1., 1. / qAbs(indicesPrPixel)); + int h = height(); + double offset = 0; + bool pathDraw = increment > 1.2; + if (increment > 1. && !pathDraw) { + pen.setWidth(ceil(increment)); + offset = pen.width() / 2.; + } else if (pathDraw) { + pen.setWidthF(0); + } + painter->setPen(pen); + QPainterPath path; if (!KdenliveSettings::displayallchannels()) { // Draw merged channels - QPainterPath path; - path.moveTo(-1, height()); double i = 0; - double increment = qMax(1., 1 / qAbs(indicesPrPixel)); double level; - int lastIdx = -1; - for (; i <= width(); i += increment) { - int idx = m_inPoint + int(i * indicesPrPixel); + int j = 0; + if (m_drawInPoint > 0) { + j = m_drawInPoint / increment; + } + if (pathDraw) { + path.moveTo(j - 1, height()); + } + for (; i <= width() && i < m_drawOutPoint; j++) { + i = j * increment; + int idx = m_precisionFactor * m_inPoint + int(i * indicesPrPixel); + idx += idx % m_channels; + i -= offset; if (idx + m_channels >= m_audioLevels.length() || idx < 0) { break; } - if (lastIdx == idx) { - continue; - } - lastIdx = idx; level = m_audioLevels.at(idx) / 255.; - for (int j = 1; j < m_channels; j++) { - level = qMax(level, m_audioLevels.at(idx + j) / 255.); + for (int k = 1; k < m_channels; k++) { + level = qMax(level, m_audioLevels.at(idx + k) / 255.); + } + if (pathDraw) { + path.lineTo(i, height() - level * height()); + } else { + painter->drawLine(i, h, i, h - (h * level)); } - path.lineTo(i, height() - level * height()); } - path.lineTo(i, height()); - painter->drawPath(path); + if (pathDraw) { + path.lineTo(i, height()); + painter->drawPath(path); + } } else { - double channelHeight = height() / (2 * m_channels); - QFont font = painter->font(); - font.setPixelSize(channelHeight - 1); - painter->setFont(font); + double channelHeight = (double)height() / m_channels; // Draw separate channels double i = 0; - double increment = qMax(1., 1 / indicesPrPixel); double level; - QRectF bgRect(0, 0, width(), 2 * channelHeight); - QVector channelPaths(m_channels); + QRectF bgRect(0, 0, width(), channelHeight); + // Path for vector drawing + //qDebug()<<"==== DRAWING FROM: "<setOpacity(0.2); if (channel % 2 == 0) { // Add dark background on odd channels - bgRect.moveTo(0, y - channelHeight); + bgRect.moveTo(0, channel * channelHeight); painter->fillRect(bgRect, Qt::black); } // Draw channel median line + painter->setOpacity(0.5); + pen.setWidthF(0); painter->setPen(pen); painter->drawLine(QLineF(0., y, width(), y)); + pen.setWidth(ceil(increment)); + painter->setPen(pathDraw ? Qt::NoPen : pen); painter->setOpacity(1); - int lastIdx = -1; - for (i = 0; i <= width(); i += increment) { - int idx = m_inPoint + ceil(i * indicesPrPixel); - if (lastIdx == idx) { - continue; - } - lastIdx = idx; + i = 0; + int j = 0; + if (m_drawInPoint > 0) { + j = m_drawInPoint / increment; + } + if (pathDraw) { + path.moveTo(m_drawInPoint - 1, y); + } + for (; i <= width() && i < m_drawOutPoint; j++) { + i = j * increment; + int idx = m_precisionFactor * m_inPoint + ceil(i * indicesPrPixel); + idx += idx % m_channels; + i -= offset; idx += channel; if (idx >= m_audioLevels.length() || idx < 0) break; - level = m_audioLevels.at(idx) * channelHeight / 255.; - channelPaths[channel].lineTo(i, y - level); + if (pathDraw) { + level = m_audioLevels.at(idx) * channelHeight / 510.; + path.lineTo(i, y - level); + } else { + level = m_audioLevels.at(idx) * channelHeight / 510.; // divide height by 510 (2*255) to get height + painter->drawLine(i, y - level, i, y + level); + } + } + if (pathDraw) { + path.lineTo(i, y); + painter->drawPath(path); + QTransform tr(1, 0, 0, -1, 0, 2 * y); + painter->drawPath(tr.map(path)); } if (m_firstChunk && m_channels > 1 && m_channels < 7) { - painter->drawText(2, y + channelHeight, chanelNames[channel]); + painter->drawText(2, y + channelHeight / 2, chanelNames[channel]); } - channelPaths[channel].lineTo(i, y); - painter->setPen(Qt::NoPen); - painter->drawPath(channelPaths.value(channel)); - QTransform tr(1, 0, 0, -1, 0, 2 * y); - painter->drawPath(tr.map(channelPaths.value(channel))); } } } signals: void levelsChanged(); void propertyChanged(); void inPointChanged(); void showItemChanged(); void audioChannelsChanged(); private: QVector m_audioLevels; int m_inPoint; int m_outPoint; + // Pixels outside the view, can be dropped + int m_drawInPoint; + int m_drawOutPoint; QString m_binId; QColor m_color; bool m_format; bool m_showItem; int m_channels; + int m_precisionFactor; int m_stream; bool m_firstChunk; }; void registerTimelineItems() { qmlRegisterType("Kdenlive.Controls", 1, 0, "TimelineTriangle"); qmlRegisterType("Kdenlive.Controls", 1, 0, "TimelinePlayhead"); qmlRegisterType("Kdenlive.Controls", 1, 0, "TimelineWaveform"); } #include "timelineitems.moc"