diff --git a/app/audio/audiovisualwaveform.cpp b/app/audio/audiovisualwaveform.cpp index e13effd5c..f6f0e4db4 100644 --- a/app/audio/audiovisualwaveform.cpp +++ b/app/audio/audiovisualwaveform.cpp @@ -90,6 +90,15 @@ void AudioVisualWaveform::OverwriteSamplesFromMipmap(const AudioVisualWaveform:: input_length = samples_length; } +void AudioVisualWaveform::ValidateVirtualStart(const rational &new_start) +{ + if (length_ == 0) { + virtual_start_ = new_start; + } else if (virtual_start_ > new_start) { + TrimIn(new_start - virtual_start_); + } +} + void AudioVisualWaveform::OverwriteSamples(const SampleBuffer &samples, int sample_rate, const rational &start) { if (!channels_) { @@ -97,18 +106,12 @@ void AudioVisualWaveform::OverwriteSamples(const SampleBuffer &samples, int samp return; } - // Old less optimized code. Keeping this around as a reference, but the below code is at least - // 10x faster so this shouldn't be used in production. - // - // size_t input_start, input_length; - // for (auto it=mipmapped_data_.begin(); it!=mipmapped_data_.end(); it++) { - // OverwriteSamplesFromBuffer(samples, sample_rate, start, it->first.toDouble(), it->second, input_start, input_length); - // } + ValidateVirtualStart(start); // Process the largest mipmap directly for the samples auto current_mipmap = mipmapped_data_.rbegin(); size_t input_start, input_length; - OverwriteSamplesFromBuffer(samples, sample_rate, start, current_mipmap->first.toDouble(), current_mipmap->second, input_start, input_length); + OverwriteSamplesFromBuffer(samples, sample_rate, start - virtual_start_, current_mipmap->first.toDouble(), current_mipmap->second, input_start, input_length); while (true) { // For each smaller mipmap, we just process from the mipmap before it, making each one @@ -120,7 +123,7 @@ void AudioVisualWaveform::OverwriteSamples(const SampleBuffer &samples, int samp } OverwriteSamplesFromMipmap(previous_mipmap->second, previous_mipmap->first.toDouble(), - input_start, input_length, start, current_mipmap->first.toDouble(), + input_start, input_length, start - virtual_start_, current_mipmap->first.toDouble(), current_mipmap->second); } @@ -130,6 +133,8 @@ void AudioVisualWaveform::OverwriteSamples(const SampleBuffer &samples, int samp void AudioVisualWaveform::OverwriteSums(const AudioVisualWaveform &sums, const rational &dest, const rational& offset, const rational& length) { + ValidateVirtualStart(dest); + for (auto it=mipmapped_data_.begin(); it!=mipmapped_data_.end(); it++) { rational rate = it->first; @@ -139,7 +144,7 @@ void AudioVisualWaveform::OverwriteSums(const AudioVisualWaveform &sums, const r double rate_dbl = rate.toDouble(); // Get our destination sample - size_t our_start_index = time_to_samples(dest, rate_dbl); + size_t our_start_index = time_to_samples(dest - virtual_start_, rate_dbl); // Get our source sample size_t their_start_index = time_to_samples(offset, rate_dbl); @@ -172,6 +177,8 @@ void AudioVisualWaveform::OverwriteSums(const AudioVisualWaveform &sums, const r void AudioVisualWaveform::OverwriteSilence(const rational &start, const rational &length) { + ValidateVirtualStart(start); + for (auto it=mipmapped_data_.begin(); it!=mipmapped_data_.end(); it++) { rational rate = it->first; @@ -180,7 +187,7 @@ void AudioVisualWaveform::OverwriteSilence(const rational &start, const rational double rate_dbl = rate.toDouble(); // Get our destination sample - size_t our_start_index = time_to_samples(start, rate_dbl); + size_t our_start_index = time_to_samples(start - virtual_start_, rate_dbl); size_t our_length_index = time_to_samples(length, rate_dbl); size_t our_end_index = our_start_index + our_length_index; @@ -190,6 +197,8 @@ void AudioVisualWaveform::OverwriteSilence(const rational &start, const rational memset(reinterpret_cast(our_arr.data()) + our_start_index * sizeof(SamplePerChannel), 0, our_length_index * sizeof(SamplePerChannel)); } + + length_ = qMax(length_, start + length); } void AudioVisualWaveform::TrimIn(rational length) @@ -198,6 +207,8 @@ void AudioVisualWaveform::TrimIn(rational length) return; } + virtual_start_ += length; + bool negative = (length < 0); if (negative) { length = -length; @@ -225,9 +236,9 @@ void AudioVisualWaveform::TrimIn(rational length) AudioVisualWaveform AudioVisualWaveform::Mid(const rational &offset) const { - AudioVisualWaveform mid = *this; + AudioVisualWaveform mid = *this; - mid.TrimIn(offset); + mid.TrimIn(offset - virtual_start_); return mid; } @@ -236,7 +247,7 @@ AudioVisualWaveform AudioVisualWaveform::Mid(const rational &offset, const ratio { AudioVisualWaveform mid = *this; - mid.TrimRange(offset, length); + mid.TrimRange(offset - virtual_start_, length); return mid; } @@ -273,7 +284,7 @@ AudioVisualWaveform::Sample AudioVisualWaveform::GetSummaryFromTime(const ration double rate_dbl = using_mipmap->first.toDouble(); - size_t start_sample = time_to_samples(start, rate_dbl); + size_t start_sample = time_to_samples(start - virtual_start_, rate_dbl); size_t sample_length = time_to_samples(length, rate_dbl); const Sample &mipmap_data = using_mipmap->second; @@ -421,7 +432,7 @@ void AudioVisualWaveform::DrawWaveform(QPainter *painter, const QRect& rect, con double rate_dbl = rate.toDouble(); const Sample& arr = using_mipmap->second; - size_t start_sample_index = samples.time_to_samples(start_time, rate_dbl); + size_t start_sample_index = samples.time_to_samples(start_time - samples.virtual_start_, rate_dbl); if (start_sample_index >= arr.size()) { return; diff --git a/app/audio/audiovisualwaveform.h b/app/audio/audiovisualwaveform.h index 8591ea11b..8f34961a1 100644 --- a/app/audio/audiovisualwaveform.h +++ b/app/audio/audiovisualwaveform.h @@ -123,6 +123,10 @@ private: std::map::const_iterator GetMipmapForScale(double scale) const; + void ValidateVirtualStart(const rational &new_start); + + rational virtual_start_; + int channels_; std::map mipmapped_data_; diff --git a/app/common/timerange.h b/app/common/timerange.h index b6235f9b8..8fc55872e 100644 --- a/app/common/timerange.h +++ b/app/common/timerange.h @@ -126,6 +126,17 @@ public: return false; } + bool OverlapsWith(const TimeRange& r, bool in_inclusive = true, bool out_inclusive = true) const + { + for (const TimeRange &range : array_) { + if (range.OverlapsWith(r, in_inclusive, out_inclusive)) { + return true; + } + } + + return false; + } + bool isEmpty() const { return array_.isEmpty(); diff --git a/app/core.cpp b/app/core.cpp index 0a90e92d4..1dd1317c2 100644 --- a/app/core.cpp +++ b/app/core.cpp @@ -1490,7 +1490,7 @@ bool Core::LabelNodes(const QVector &nodes, MultiUndoCommand *parent) return false; } -Sequence *Core::CreateNewSequenceForProject(Project* project) const +Sequence *Core::CreateNewSequenceForProject(const QString &format, Project* project) { Sequence* new_sequence = new Sequence(); @@ -1498,7 +1498,7 @@ Sequence *Core::CreateNewSequenceForProject(Project* project) const int sequence_number = 1; QString sequence_name; do { - sequence_name = tr("Sequence %1").arg(sequence_number); + sequence_name = format.arg(sequence_number); sequence_number++; } while (project->root()->ChildExistsWithName(sequence_name)); new_sequence->SetLabel(sequence_name); diff --git a/app/core.h b/app/core.h index a2ded04db..0d9d8df0d 100644 --- a/app/core.h +++ b/app/core.h @@ -255,7 +255,11 @@ public: /** * @brief Create a new sequence named appropriately for the active project */ - Sequence* CreateNewSequenceForProject(Project *project) const; + static Sequence* CreateNewSequenceForProject(const QString &format, Project *project); + static Sequence* CreateNewSequenceForProject(Project *project) + { + return CreateNewSequenceForProject(tr("Sequence %1"), project); + } /** * @brief Opens a project from the recently opened list diff --git a/app/node/block/clip/clip.cpp b/app/node/block/clip/clip.cpp index 787cf5442..2d3ac32cb 100644 --- a/app/node/block/clip/clip.cpp +++ b/app/node/block/clip/clip.cpp @@ -232,7 +232,7 @@ void ClipBlock::RequestRangeFromConnected(const TimeRange &range) { TimeRange thumb_range = range.Intersected(max_range); if (GetAdjustedThumbnailRange(&thumb_range)) { - emit connected->thumbnail_cache()->Request(thumb_range); + connected->thumbnail_cache()->Request(thumb_range); } } @@ -296,7 +296,7 @@ void ClipBlock::RequestRangeForCache(PlaybackCache *cache, const TimeRange &max_ } if (request) { - emit cache->Request(r); + cache->Request(r); } } @@ -541,4 +541,14 @@ TimeRange ClipBlock::media_range() const return InputTimeAdjustment(kBufferIn, -1, TimeRange(0, length())); } +MultiCamNode *ClipBlock::FindMulticam() +{ + auto v = FindInputNodesConnectedToInput(NodeInput(this, kBufferIn)); + if (v.empty()) { + return nullptr; + } else { + return v.first(); + } +} + } diff --git a/app/node/block/clip/clip.h b/app/node/block/clip/clip.h index 5c84a1eca..15cadcfb7 100644 --- a/app/node/block/clip/clip.h +++ b/app/node/block/clip/clip.h @@ -24,6 +24,7 @@ #include "audio/audiovisualwaveform.h" #include "codec/decoder.h" #include "node/block/block.h" +#include "node/input/multicam/multicamnode.h" #include "node/output/track/track.h" namespace olive { @@ -197,6 +198,8 @@ public: SetStandardValue(kLoopModeInput, int(l)); } + MultiCamNode *FindMulticam(); + static const QString kBufferIn; static const QString kMediaInInput; static const QString kSpeedInput; diff --git a/app/node/factory.cpp b/app/node/factory.cpp index b67090ba8..1839d8f08 100644 --- a/app/node/factory.cpp +++ b/app/node/factory.cpp @@ -53,6 +53,7 @@ #include "generator/text/textv1.h" #include "generator/text/textv2.h" #include "generator/text/textv3.h" +#include "input/multicam/multicamnode.h" #include "input/time/timeinput.h" #include "input/value/valuenode.h" #include "keying/chromakey/chromakey.h" @@ -306,6 +307,8 @@ Node *NodeFactory::CreateFromFactoryIndex(const NodeFactory::InternalID &id) return new SwirlDistortNode(); case kRippleDistort: return new RippleDistortNode(); + case kMulticamNode: + return new MultiCamNode(); case kInternalNodeCount: break; diff --git a/app/node/factory.h b/app/node/factory.h index 192765ab4..fbf353551 100644 --- a/app/node/factory.h +++ b/app/node/factory.h @@ -80,6 +80,7 @@ public: kRippleDistort, kTileDistort, kSwirlDistort, + kMulticamNode, // Count value kInternalNodeCount diff --git a/app/node/generator/polygon/polygon.cpp b/app/node/generator/polygon/polygon.cpp index 82a823556..b482392d6 100644 --- a/app/node/generator/polygon/polygon.cpp +++ b/app/node/generator/polygon/polygon.cpp @@ -116,9 +116,9 @@ void PolygonGenerator::GenerateFrame(FramePtr frame, const GenerateJob &job) con QImage img((uchar *) frame->data(), frame->width(), frame->height(), frame->linesize_bytes(), QImage::Format_RGBA8888_Premultiplied); img.fill(Qt::transparent); - QVector points = job.Get(kPointsInput).value< QVector >(); + auto points = job.Get(kPointsInput).toArray(); - QPainterPath path = GeneratePath(points); + QPainterPath path = GeneratePath(points, InputArraySize(kPointsInput)); QPainter p(&img); double par = frame->video_params().pixel_aspect_ratio().toDouble(); @@ -175,7 +175,7 @@ void PolygonGenerator::UpdateGizmoPositions(const NodeValueRow &row, const NodeG QPointF half_res = res.toPointF()/2; - QVector points = row[kPointsInput].value< QVector >(); + auto points = row[kPointsInput].toArray(); int current_pos_sz = gizmo_position_handles_.size(); @@ -200,8 +200,9 @@ void PolygonGenerator::UpdateGizmoPositions(const NodeValueRow &row, const NodeG bez_gizmo2->SetSmaller(true); } - if (!points.isEmpty()) { - for (int i=0; iSetPath(GeneratePath(points).translated(half_res)); + poly_gizmo_->SetPath(GeneratePath(points, pts_sz).translated(half_res)); } ShaderCode PolygonGenerator::GetShaderCode(const ShaderRequest &request) const @@ -250,19 +251,19 @@ void PolygonGenerator::AddPointToPath(QPainterPath *path, const Bezier &before, after.ToPointF()); } -QPainterPath PolygonGenerator::GeneratePath(const QVector &points) +QPainterPath PolygonGenerator::GeneratePath(const NodeValueArray &points, int size) { QPainterPath path; - if (!points.isEmpty()) { - const Bezier &first_pt = points.first().toBezier(); + if (!points.empty()) { + const Bezier &first_pt = points.at(0).toBezier(); path.moveTo(first_pt.ToPointF()); - for (int i=1; i &points); + static QPainterPath GeneratePath(const NodeValueArray &points, int size); template void ValidateGizmoVectorSize(QVector &vec, int new_sz); diff --git a/app/node/generator/shape/generatorwithmerge.cpp b/app/node/generator/shape/generatorwithmerge.cpp index 8622c1881..93f8bcde3 100644 --- a/app/node/generator/shape/generatorwithmerge.cpp +++ b/app/node/generator/shape/generatorwithmerge.cpp @@ -59,7 +59,7 @@ void GeneratorWithMerge::PushMergableJob(const NodeValueRow &value, TexturePtr j merge.SetShaderID(QStringLiteral("mrg")); merge.Insert(MergeNode::kBaseIn, value[kBaseInput]); - merge.Insert(MergeNode::kBlendIn, NodeValue(NodeValue::kTexture, base->toJob(*job->job()), this)); + merge.Insert(MergeNode::kBlendIn, NodeValue(NodeValue::kTexture, job, this)); table->Push(NodeValue::kTexture, base->toJob(merge), this); } else { diff --git a/app/node/generator/text/textv3.cpp b/app/node/generator/text/textv3.cpp index afe900778..1964f8cb0 100644 --- a/app/node/generator/text/textv3.cpp +++ b/app/node/generator/text/textv3.cpp @@ -105,7 +105,7 @@ void TextGeneratorV3::Value(const NodeValueRow &value, const NodeGlobals &global if (!args.empty()) { QStringList list; list.reserve(args.size()); - for (int i=0; i #include "common/timerange.h" +#include "render/audioparams.h" #include "render/videoparams.h" namespace olive { @@ -33,19 +34,22 @@ class NodeGlobals public: NodeGlobals(){} - NodeGlobals(const VideoParams &vparam, const TimeRange &time) : + NodeGlobals(const VideoParams &vparam, const AudioParams &aparam, const TimeRange &time) : video_params_(vparam), + audio_params_(aparam), time_(time) { } QVector2D square_resolution() const { return video_params_.square_resolution(); } QVector2D nonsquare_resolution() const { return video_params_.resolution(); } + const AudioParams &aparams() const { return audio_params_; } const VideoParams &vparams() const { return video_params_; } const TimeRange &time() const { return time_; } private: VideoParams video_params_; + AudioParams audio_params_; TimeRange time_; }; diff --git a/app/node/input/CMakeLists.txt b/app/node/input/CMakeLists.txt index 6de136173..ab2b3569e 100644 --- a/app/node/input/CMakeLists.txt +++ b/app/node/input/CMakeLists.txt @@ -14,6 +14,7 @@ # You should have received a copy of the GNU General Public License # along with this program. If not, see . +add_subdirectory(multicam) add_subdirectory(time) add_subdirectory(value) diff --git a/app/node/input/multicam/CMakeLists.txt b/app/node/input/multicam/CMakeLists.txt new file mode 100644 index 000000000..fca12be16 --- /dev/null +++ b/app/node/input/multicam/CMakeLists.txt @@ -0,0 +1,22 @@ +# Olive - Non-Linear Video Editor +# Copyright (C) 2022 Olive Team +# +# 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 . + +set(OLIVE_SOURCES + ${OLIVE_SOURCES} + node/input/multicam/multicamnode.h + node/input/multicam/multicamnode.cpp + PARENT_SCOPE +) diff --git a/app/node/input/multicam/multicamnode.cpp b/app/node/input/multicam/multicamnode.cpp index 0c1273cb5..8bdb5a5dd 100644 --- a/app/node/input/multicam/multicamnode.cpp +++ b/app/node/input/multicam/multicamnode.cpp @@ -1,6 +1,98 @@ #include "multicamnode.h" +namespace olive { + +#define super Node + +const QString MultiCamNode::kCurrentInput = QStringLiteral("current_in"); +const QString MultiCamNode::kSourcesInput = QStringLiteral("sources_in"); + MultiCamNode::MultiCamNode() { + AddInput(kCurrentInput, NodeValue::kInt, InputFlags(kInputFlagStatic)); + + // Make current index start at 1 instead of 0 + SetInputProperty(kCurrentInput, QStringLiteral("offset"), 1); + SetInputProperty(kCurrentInput, QStringLiteral("min"), 0); + + AddInput(kSourcesInput, NodeValue::kNone, InputFlags(kInputFlagNotKeyframable | kInputFlagArray)); + SetInputProperty(kSourcesInput, QStringLiteral("arraystart"), 1); +} + +QString MultiCamNode::Name() const +{ + return tr("Multi-Cam"); +} + +QString MultiCamNode::id() const +{ + return QStringLiteral("org.olivevideoeditor.Olive.multicam"); +} + +QVector MultiCamNode::Category() const +{ + return {kCategoryTimeline}; +} + +QString MultiCamNode::Description() const +{ + return tr("Allows easy switching between multiple sources."); +} + +Node::ActiveElements MultiCamNode::GetActiveElementsAtTime(const QString &input, const TimeRange &r) const +{ + if (input == kSourcesInput) { + int src = GetCurrentSource(); + if (src >= 0 && src < InputArraySize(kSourcesInput)) { + Node::ActiveElements a; + a.add(src); + return a; + } else { + return ActiveElements::kNoElements; + } + } else { + return super::GetActiveElementsAtTime(input, r); + } +} + +void MultiCamNode::Value(const NodeValueRow &value, const NodeGlobals &globals, NodeValueTable *table) const +{ + NodeValueArray arr = value[kSourcesInput].toArray(); + if (!arr.empty()) { + table->Push(arr.begin()->second); + } +} + +void MultiCamNode::IndexToRowCols(int index, int total_rows, int total_cols, int *row, int *col) +{ + Q_UNUSED(total_rows) + + *col = index%total_cols; + *row = index/total_cols; +} + +void MultiCamNode::Retranslate() +{ + super::Retranslate(); + + SetInputName(kCurrentInput, tr("Current")); + SetInputName(kSourcesInput, tr("Sources")); +} + +void MultiCamNode::GetRowsAndColumns(int sources, int *rows_in, int *cols_in) +{ + int &rows = *rows_in; + int &cols = *cols_in; + + rows = 1; + cols = 1; + while (rows*cols < sources) { + if (rows < cols) { + rows++; + } else { + cols++; + } + } +} } diff --git a/app/node/input/multicam/multicamnode.h b/app/node/input/multicam/multicamnode.h index c21e7f6ca..56a9ed858 100644 --- a/app/node/input/multicam/multicamnode.h +++ b/app/node/input/multicam/multicamnode.h @@ -1,11 +1,57 @@ #ifndef MULTICAMNODE_H #define MULTICAMNODE_H +#include "node/node.h" -class MultiCamNode +namespace olive { + +class MultiCamNode : public Node { + Q_OBJECT public: MultiCamNode(); + + NODE_DEFAULT_FUNCTIONS(MultiCamNode) + + virtual QString Name() const override; + virtual QString id() const override; + virtual QVector Category() const override; + virtual QString Description() const override; + + virtual ActiveElements GetActiveElementsAtTime(const QString &input, const TimeRange &r) const override; + + virtual void Value(const NodeValueRow& value, const NodeGlobals &globals, NodeValueTable *table) const override; + + virtual void Retranslate() override; + + static const QString kCurrentInput; + static const QString kSourcesInput; + + int GetCurrentSource() const + { + return GetStandardValue(kCurrentInput).toInt(); + } + + int GetSourceCount() const + { + return InputArraySize(kSourcesInput); + } + + static void GetRowsAndColumns(int sources, int *rows, int *cols); + void GetRowsAndColumns(int *rows, int *cols) const + { + return GetRowsAndColumns(GetSourceCount(), rows, cols); + } + + static void IndexToRowCols(int index, int total_rows, int total_cols, int *row, int *col); + + static int RowsColsToIndex(int row, int col, int total_rows, int total_cols) + { + return col + row * total_cols; + } + }; +} + #endif // MULTICAMNODE_H diff --git a/app/node/node.cpp b/app/node/node.cpp index 1c2ec5c4e..c677b130f 100644 --- a/app/node/node.cpp +++ b/app/node/node.cpp @@ -27,11 +27,8 @@ #include "common/bezier.h" #include "common/lerp.h" -#include "common/timecodefunctions.h" -#include "common/xmlutils.h" #include "core.h" #include "config/config.h" -#include "node/project/footage/footage.h" #include "project/project.h" #include "ui/colorcoding.h" #include "ui/icons/icons.h" @@ -251,7 +248,7 @@ QString Node::GetInputName(const QString &id) const if (i) { return i->human_name; } else { - ReportInvalidInput("get name of", id); + ReportInvalidInput("get name of", id, -1); return QString(); } } @@ -278,7 +275,7 @@ bool Node::IsInputKeyframing(const QString &input, int element) const if (imm) { return imm->is_keyframing(); } else { - ReportInvalidInput("get keyframing state of", input); + ReportInvalidInput("get keyframing state of", input, element); return false; } } @@ -297,7 +294,7 @@ void Node::SetInputIsKeyframing(const QString &input, bool e, int element) emit KeyframeEnableChanged(NodeInput(this, input, element), e); } else { - ReportInvalidInput("set keyframing state of", input); + ReportInvalidInput("set keyframing state of", input, element); } } @@ -324,7 +321,7 @@ bool Node::IsUsingStandardValue(const QString &input, int track, int element) co if (imm) { return imm->is_using_standard_value(track); } else { - ReportInvalidInput("determine whether using standard value in", input); + ReportInvalidInput("determine whether using standard value in", input, element); return true; } } @@ -336,7 +333,7 @@ NodeValue::Type Node::GetInputDataType(const QString &id) const if (i) { return i->type; } else { - ReportInvalidInput("get data type of", id); + ReportInvalidInput("get data type of", id, -1); return NodeValue::kNone; } } @@ -355,7 +352,7 @@ void Node::SetInputDataType(const QString &id, const NodeValue::Type &type) emit InputDataTypeChanged(id, type); } else { - ReportInvalidInput("set data type of", id); + ReportInvalidInput("set data type of", id, -1); } } @@ -366,7 +363,7 @@ bool Node::HasInputProperty(const QString &id, const QString &name) const if (i) { return i->properties.contains(name); } else { - ReportInvalidInput("get property of", id); + ReportInvalidInput("get property of", id, -1); return false; } } @@ -378,7 +375,7 @@ QHash Node::GetInputProperties(const QString &id) const if (i) { return i->properties; } else { - ReportInvalidInput("get property table of", id); + ReportInvalidInput("get property table of", id, -1); return QHash(); } } @@ -390,7 +387,7 @@ QVariant Node::GetInputProperty(const QString &id, const QString &name) const if (i) { return i->properties.value(name); } else { - ReportInvalidInput("get property of", id); + ReportInvalidInput("get property of", id, -1); return QVariant(); } } @@ -404,7 +401,7 @@ void Node::SetInputProperty(const QString &id, const QString &name, const QVaria emit InputPropertyChanged(id, name, value); } else { - ReportInvalidInput("set property of", id); + ReportInvalidInput("set property of", id, -1); } } @@ -548,7 +545,7 @@ SplitValue Node::GetSplitDefaultValue(const QString &input) const if (i) { return i->default_value; } else { - ReportInvalidInput("retrieve default value of", input); + ReportInvalidInput("retrieve default value of", input, -1); return SplitValue(); } } @@ -577,7 +574,7 @@ void Node::SetSplitDefaultValue(const QString &input, const SplitValue &val) if (i) { i->default_value = val; } else { - ReportInvalidInput("set default value of", input); + ReportInvalidInput("set default value of", input, -1); } } @@ -590,7 +587,7 @@ void Node::SetSplitDefaultValueOnTrack(const QString &input, const QVariant &val i->default_value[track] = val; } } else { - ReportInvalidInput("set default value on track of", input); + ReportInvalidInput("set default value on track of", input, -1); } } @@ -606,7 +603,7 @@ QVector Node::GetKeyframesAtTime(const QString &input, const rat if (imm) { return imm->get_keyframe_at_time(time); } else { - ReportInvalidInput("get keyframes at time from", input); + ReportInvalidInput("get keyframes at time from", input, element); return QVector(); } } @@ -618,7 +615,7 @@ NodeKeyframe *Node::GetKeyframeAtTimeOnTrack(const QString &input, const rationa if (imm) { return imm->get_keyframe_at_time_on_track(time, track); } else { - ReportInvalidInput("get keyframe at time on track from", input); + ReportInvalidInput("get keyframe at time on track from", input, element); return nullptr; } } @@ -630,7 +627,7 @@ NodeKeyframe::Type Node::GetBestKeyframeTypeForTimeOnTrack(const QString &input, if (imm) { return imm->get_best_keyframe_type_for_time(time, track); } else { - ReportInvalidInput("get closest keyframe before a time from", input); + ReportInvalidInput("get closest keyframe before a time from", input, element); return NodeKeyframe::kDefaultType; } } @@ -647,7 +644,7 @@ NodeKeyframe *Node::GetEarliestKeyframe(const QString &id, int element) const if (imm) { return imm->get_earliest_keyframe(); } else { - ReportInvalidInput("get earliest keyframe from", id); + ReportInvalidInput("get earliest keyframe from", id, element); return nullptr; } } @@ -659,7 +656,7 @@ NodeKeyframe *Node::GetLatestKeyframe(const QString &id, int element) const if (imm) { return imm->get_latest_keyframe(); } else { - ReportInvalidInput("get latest keyframe from", id); + ReportInvalidInput("get latest keyframe from", id, element); return nullptr; } } @@ -671,7 +668,7 @@ NodeKeyframe *Node::GetClosestKeyframeBeforeTime(const QString &id, const ration if (imm) { return imm->get_closest_keyframe_before_time(time); } else { - ReportInvalidInput("get closest keyframe before a time from", id); + ReportInvalidInput("get closest keyframe before a time from", id, element); return nullptr; } } @@ -683,7 +680,7 @@ NodeKeyframe *Node::GetClosestKeyframeAfterTime(const QString &id, const rationa if (imm) { return imm->get_closest_keyframe_after_time(time); } else { - ReportInvalidInput("get closest keyframe after a time from", id); + ReportInvalidInput("get closest keyframe after a time from", id, element); return nullptr; } } @@ -695,7 +692,7 @@ bool Node::HasKeyframeAtTime(const QString &id, const rational &time, int elemen if (imm) { return imm->has_keyframe_at_time(time); } else { - ReportInvalidInput("determine if it has a keyframe at a time from", id); + ReportInvalidInput("determine if it has a keyframe at a time from", id, element); return false; } } @@ -719,7 +716,7 @@ SplitValue Node::GetSplitStandardValue(const QString &id, int element) const if (imm) { return imm->get_split_standard_value(); } else { - ReportInvalidInput("get standard value of", id); + ReportInvalidInput("get standard value of", id, element); return SplitValue(); } } @@ -731,7 +728,7 @@ QVariant Node::GetSplitStandardValueOnTrack(const QString &input, int track, int if (imm) { return imm->get_split_standard_value_on_track(track); } else { - ReportInvalidInput("get standard value of", input); + ReportInvalidInput("get standard value of", input, element); return QVariant(); } } @@ -758,7 +755,7 @@ void Node::SetSplitStandardValue(const QString &id, const SplitValue &value, int } } } else { - ReportInvalidInput("set standard value of", id); + ReportInvalidInput("set standard value of", id, element); } } @@ -774,7 +771,7 @@ void Node::SetSplitStandardValueOnTrack(const QString &id, int track, const QVar ParameterValueChanged(id, element, TimeRange(RATIONAL_MIN, RATIONAL_MAX)); } } else { - ReportInvalidInput("set standard value of", id); + ReportInvalidInput("set standard value of", id, element); } } @@ -873,7 +870,7 @@ int Node::InputArraySize(const QString &id) const if (i) { return i->array_size; } else { - ReportInvalidInput("retrieve array size of", id); + ReportInvalidInput("retrieve array size of", id, -1); return 0; } } @@ -914,7 +911,7 @@ InputFlags Node::GetInputFlags(const QString &input) const if (i) { return i->flags; } else { - ReportInvalidInput("retrieve flags of", input); + ReportInvalidInput("retrieve flags of", input, -1); return InputFlags(kInputFlagNormal); } } @@ -927,7 +924,7 @@ void Node::SetInputFlags(const QString &input, const InputFlags &f) i->flags = f; emit InputFlagsChanged(input, i->flags); } else { - ReportInvalidInput("set flags of", input); + ReportInvalidInput("set flags of", input, -1); } } @@ -1227,7 +1224,7 @@ void Node::RemoveInput(const QString &id) int index = input_ids_.indexOf(id); if (index == -1) { - ReportInvalidInput("remove", id); + ReportInvalidInput("remove", id, -1); return; } @@ -1237,9 +1234,9 @@ void Node::RemoveInput(const QString &id) emit InputRemoved(id); } -void Node::ReportInvalidInput(const char *attempted_action, const QString& id) const +void Node::ReportInvalidInput(const char *attempted_action, const QString& id, int element) const { - qWarning() << "Failed to" << attempted_action << "parameter" << id + qWarning() << "Failed to" << attempted_action << "parameter" << id << "element" << element << "in node" << this->id() << "- input doesn't exist"; } @@ -1250,7 +1247,7 @@ NodeInputImmediate *Node::CreateImmediate(const QString &input) if (i) { return new NodeInputImmediate(i->type, i->default_value); } else { - ReportInvalidInput("create immediate", input); + ReportInvalidInput("create immediate", input, -1); return nullptr; } } @@ -1260,7 +1257,7 @@ void Node::ArrayResizeInternal(const QString &id, int size) Input* imm = GetInternalInputData(id); if (!imm) { - ReportInvalidInput("set array size", id); + ReportInvalidInput("set array size", id, -1); return; } @@ -1299,7 +1296,7 @@ void Node::SetInputName(const QString &id, const QString &name) emit InputNameChanged(id, name); } else { - ReportInvalidInput("set name of", id); + ReportInvalidInput("set name of", id, -1); } } @@ -1426,8 +1423,8 @@ void Node::CopyValuesOfElement(const Node *src, Node *dst, const QString &input, } } - foreach (const NodeKeyframeTrack& track, src->GetImmediate(input, src_element)->keyframe_tracks()) { - foreach (NodeKeyframe* key, track) { + for (const NodeKeyframeTrack& track : src->GetImmediate(input, src_element)->keyframe_tracks()) { + for (NodeKeyframe* key : track) { NodeKeyframe *copy = key->copy(dst_element, command ? nullptr : dst); if (command) { command->add_child(new NodeParamInsertKeyframeCommand(dst, copy)); diff --git a/app/node/node.h b/app/node/node.h index 2ca325d34..3ac69a6f9 100644 --- a/app/node/node.h +++ b/app/node/node.h @@ -212,6 +212,40 @@ public: return input_ids_; } + class ActiveElements + { + public: + enum Mode { + kAllElements, + kSpecified, + kNoElements + }; + + ActiveElements(Mode m = kAllElements) + { + mode_ = m; + } + + Mode mode() const { return mode_; } + std::list elements() const { return elements_; } + + void add(int e) + { + elements_.push_back(e); + mode_ = kSpecified; + } + + private: + Mode mode_; + std::list elements_; + + }; + + virtual ActiveElements GetActiveElementsAtTime(const QString &input, const TimeRange &r) const + { + return ActiveElements::kAllElements; + } + bool HasInputWithID(const QString& id) const { return input_ids_.contains(id); @@ -1318,7 +1352,7 @@ private: } } - void ReportInvalidInput(const char* attempted_action, const QString &id) const; + void ReportInvalidInput(const char* attempted_action, const QString &id, int element) const; void ArrayResizeInternal(const QString& id, int size); diff --git a/app/node/output/track/track.cpp b/app/node/output/track/track.cpp index b2d3775f0..f2bed9064 100644 --- a/app/node/output/track/track.cpp +++ b/app/node/output/track/track.cpp @@ -24,8 +24,10 @@ #include #include +#include "audio/audioprocessor.h" +#include "node/block/clip/clip.h" #include "node/block/gap/gap.h" -#include "node/graph.h" +#include "node/block/transition/transition.h" namespace olive { @@ -95,6 +97,55 @@ QString Track::Description() const "a Sequence."); } +Node::ActiveElements Track::GetActiveElementsAtTime(const QString &input, const TimeRange &r) const +{ + if (input == kBlockInput) { + if (IsMuted() || blocks_.empty() || r.in() >= track_length() || r.out() <= 0) { + return ActiveElements::kNoElements; + } else { + int start = GetBlockIndexAtTime(r.in()); + int end = GetBlockIndexAtTime(r.out()); + + if (start == -1) { + start = 0; + } + if (end == -1) { + end = blocks_.size()-1; + } + + ActiveElements a; + for (int i=start; i<=end; i++) { + Block *b = blocks_.at(i); + if (b->is_enabled() && (dynamic_cast(b) || dynamic_cast(b))) { + a.add(GetArrayIndexFromCacheIndex(i)); + } + } + + if (a.elements().empty()) { + return ActiveElements::kNoElements; + } else { + return a; + } + } + } else { + return super::GetActiveElementsAtTime(input, r); + } +} + +void Track::Value(const NodeValueRow &value, const NodeGlobals &globals, NodeValueTable *table) const +{ + if (this->type() == Track::kVideo) { + // Just pass straight through + NodeValueArray a = value[kBlockInput].toArray(); + if (!a.empty()) { + table->Push(a.begin()->second); + } + } else if (this->type() == Track::kAudio) { + // Audio + ProcessAudioTrack(value, globals, table); + } +} + TimeRange Track::InputTimeAdjustment(const QString& input, int element, const TimeRange& input_time) const { if (input == kBlockInput && element >= 0) { @@ -352,56 +403,31 @@ Block *Track::NearestBlockAfter(const rational &time) const return nullptr; } -Block *Track::BlockAtTime(const rational &time) const +bool Track::IsRangeFree(const TimeRange &range) const { - if (IsMuted() || time > track_length() || blocks_.isEmpty()) { - return nullptr; + Block *b = NearestBlockBeforeOrAt(range.in()); + if (!b) { + // No block here, assume track is empty here + return true; } - // Use binary search to find block at time - Block* using_block = nullptr; + if (!dynamic_cast(b)) { + // There's a block at or around the start point that isn't a gap, range is not free + return false; + } - int low = 0; - int high = blocks_.size() - 1; - while (low <= high) { - int mid = low + (high - low) / 2; - - Block* block = blocks_.at(mid); - if (block->in() <= time && block->out() > time) { - using_block = block; + while ((b = b->next())) { + if (b->in() >= range.out()) { + // This block is after the range, no longer relevant break; - } else if (block->out() <= time) { - low = mid + 1; - } else { - high = mid - 1; + } else if (!dynamic_cast(b)) { + // Found a block in this range, range is not free + return false; } } - if (using_block && !using_block->is_enabled()) { - using_block = nullptr; - } - - return using_block; -} - -QVector Track::BlocksAtTimeRange(const TimeRange &range) const -{ - QVector list; - - if (IsMuted()) { - return list; - } - - foreach (Block* block, blocks_) { - if (block - && block->is_enabled() - && block->out() > range.in() - && block->in() < range.out()) { - list.append(block); - } - } - - return list; + // If we get here, we couldn't find anything in the way of this range + return true; } void Track::InvalidateCache(const TimeRange& range, const QString& from, int element, InvalidateCacheOptions options) @@ -579,6 +605,124 @@ int Track::GetCacheIndexFromArrayIndex(int index) const return block_array_indexes_.indexOf(index); } +int Track::GetBlockIndexAtTime(const rational &time) const +{ + if (time < 0 || time >= track_length()) { + return -1; + } + + // Use binary search to find block at time + int low = 0; + int high = blocks_.size() - 1; + while (low <= high) { + int mid = low + (high - low) / 2; + + Block* block = blocks_.at(mid); + if (block->in() <= time && block->out() > time) { + return mid; + } else if (block->out() <= time) { + low = mid + 1; + } else { + high = mid - 1; + } + } + + return -1; +} + +void Track::ProcessAudioTrack(const NodeValueRow &value, const NodeGlobals &globals, NodeValueTable *table) const +{ + const TimeRange &range = globals.time(); + + // All these blocks will need to output to a buffer so we create one here + SampleBuffer block_range_buffer(globals.aparams(), range.length()); + block_range_buffer.silence(); + + // Loop through active blocks retrieving their audio + NodeValueArray arr = value[kBlockInput].toArray(); + + for (auto it=arr.cbegin(); it!=arr.cend(); it++) { + Block *b = blocks_.at(GetCacheIndexFromArrayIndex(it->first)); + + TimeRange range_for_block(qMax(b->in(), range.in()), + qMin(b->out(), range.out())); + + qint64 destination_offset = globals.aparams().time_to_samples(range_for_block.in() - range.in()); + qint64 max_dest_sz = globals.aparams().time_to_samples(range_for_block.length()); + + // Destination buffer + SampleBuffer samples_from_this_block = it->second.toSamples(); + ClipBlock *clip_cast = dynamic_cast(b); + + if (samples_from_this_block.is_allocated()) { + // If this is a clip, we might have extra speed/reverse information + if (clip_cast) { + double speed_value = clip_cast->speed(); + bool reversed = clip_cast->reverse(); + + if (qIsNull(speed_value)) { + // Just silence, don't think there's any other practical application of 0 speed audio + samples_from_this_block.silence(); + } else if (!qFuzzyCompare(speed_value, 1.0)) { + if (clip_cast->maintain_audio_pitch()) { + AudioProcessor processor; + + if (processor.Open(samples_from_this_block.audio_params(), samples_from_this_block.audio_params(), speed_value)) { + AudioProcessor::Buffer out; + + // FIXME: This is not the best way to do this, the TempoProcessor works best + // when it's given a continuous stream of audio, which is challenging + // in our current "modular" audio system. This should still work reasonably + // well on export (assuming audio is all generated at once on export), but + // users may hear clicks and pops in the audio during preview due to this + // approach. + int r = processor.Convert(samples_from_this_block.to_raw_ptrs().data(), samples_from_this_block.sample_count(), nullptr); + + if (r < 0) { + qCritical() << "Failed to change tempo of audio:" << r; + } else { + processor.Flush(); + + processor.Convert(nullptr, 0, &out); + + if (!out.empty()) { + int nb_samples = out.front().size() * samples_from_this_block.audio_params().bytes_per_sample_per_channel(); + + if (nb_samples) { + SampleBuffer new_samples(samples_from_this_block.audio_params(), nb_samples); + + for (int i=0; iPush(NodeValue::kSamples, QVariant::fromValue(block_range_buffer), this); +} + void Track::BlockLengthChanged() { // Assumes sender is a Block diff --git a/app/node/output/track/track.h b/app/node/output/track/track.h index 4994bf73c..35c23dfb8 100644 --- a/app/node/output/track/track.h +++ b/app/node/output/track/track.h @@ -22,7 +22,6 @@ #define TRACK_H #include "node/block/block.h" -#include "timeline/timelinecommon.h" namespace olive { @@ -55,6 +54,9 @@ public: virtual QVector Category() const override; virtual QString Description() const override; + virtual ActiveElements GetActiveElementsAtTime(const QString &input, const TimeRange &r) const override; + virtual void Value(const NodeValueRow& value, const NodeGlobals &globals, NodeValueTable *table) const override; + virtual TimeRange InputTimeAdjustment(const QString& input, int element, const TimeRange& input_time) const override; virtual TimeRange OutputTimeAdjustment(const QString& input, int element, const TimeRange& input_time) const override; @@ -314,29 +316,10 @@ public: */ Block* NearestBlockAfter(const rational& time) const; - /** - * @brief Returns the block that should be rendered/visible at a given time - * - * Use this for any video rendering or determining which block will actually be active at any - * time. - * - * @return Catches the first block that matches `block.in <= time && block.out > time`. Returns - * nullptr if the time exceeds the track length, the block active at this time is disabled, or - * if IsMuted() is true. + /* + * @brief Returns whether a time range is empty or only has a gap */ - Block* BlockAtTime(const rational& time) const; - - /** - * @brief Returns a list of blocks that should be rendered/visible during a given time range - * - * Use this for audio rendering to determine all blocks that will be active throughout a range - * of time. - * - * @return Similar to BlockAtTime() but will match several blocks where - * `block.in < range.out && block.out > range.in`. Returns an empty list if IsMuted() or if - * `range.in >= track.length`. Blocks that are not enabled will be omitted from the returned list. - */ - QVector BlocksAtTimeRange(const TimeRange& range) const; + bool IsRangeFree(const TimeRange &range) const; const QVector &Blocks() const { @@ -345,6 +328,12 @@ public: virtual void InvalidateCache(const TimeRange& range, const QString& from, int element, InvalidateCacheOptions options) override; + Block *VisibleBlockAtTime(const rational &t) const + { + int index = GetBlockIndexAtTime(t); + return (index == -1) ? nullptr : blocks_.at(index); + } + /** * @brief Adds Block `block` at the very beginning of the Sequence before all other clips */ @@ -467,6 +456,10 @@ private: int GetCacheIndexFromArrayIndex(int index) const; + int GetBlockIndexAtTime(const rational &time) const; + + void ProcessAudioTrack(const NodeValueRow &value, const NodeGlobals &globals, NodeValueTable *table) const; + TimeRangeList block_length_pending_invalidations_; QVector blocks_; diff --git a/app/node/output/viewer/viewer.cpp b/app/node/output/viewer/viewer.cpp index 8c9456626..34ec101d6 100644 --- a/app/node/output/viewer/viewer.cpp +++ b/app/node/output/viewer/viewer.cpp @@ -224,16 +224,16 @@ void ViewerOutput::InvalidateCache(const TimeRange& range, const QString& from, if (Node *connected = GetConnectedOutput(from, element)) { if (from == kTextureInput) { - //emit connected->thumbnail_cache()->Request(range.Intersected(max_range), PlaybackCache::kPreviewsOnly); + //connected->thumbnail_cache()->Request(range.Intersected(max_range), PlaybackCache::kPreviewsOnly); if (autocache_input_video_) { TimeRange max_range = InputTimeAdjustment(from, element, TimeRange(0, GetVideoLength())); - emit connected->video_frame_cache()->Request(range.Intersected(max_range)); + connected->video_frame_cache()->Request(range.Intersected(max_range)); } } else if (from == kSamplesInput) { TimeRange max_range = InputTimeAdjustment(from, element, TimeRange(0, GetAudioLength())); - emit connected->waveform_cache()->Request(range.Intersected(max_range)); + connected->waveform_cache()->Request(range.Intersected(max_range)); if (autocache_input_audio_) { - emit connected->audio_playback_cache()->Request(range.Intersected(max_range)); + connected->audio_playback_cache()->Request(range.Intersected(max_range)); } } } @@ -389,11 +389,11 @@ Node::ValueHint ViewerOutput::GetConnectedSampleValueHint() void ViewerOutput::ConnectedToPreviewEvent() { - if (Node *connected = GetConnectedOutput(kSamplesInput)) { + if (Node *connected = this->GetConnectedSampleOutput()) { TimeRange max_range = InputTimeAdjustment(kSamplesInput, -1, TimeRange(0, GetAudioLength())); TimeRangeList invalid = connected->waveform_cache()->GetInvalidatedRanges(max_range); for (const TimeRange &r : invalid) { - emit connected->waveform_cache()->Request(r); + connected->waveform_cache()->Request(r); } } } diff --git a/app/node/traverser.cpp b/app/node/traverser.cpp index 0749d3c80..bb95a6463 100644 --- a/app/node/traverser.cpp +++ b/app/node/traverser.cpp @@ -88,11 +88,11 @@ NodeValue NodeTraverser::GenerateRowValue(const Node *node, const QString &input if (value.array()) { // Resolve each element of array - QVector tables = value.value >(); - QVector output(tables.size()); + NodeValueTableArray tables = value.value(); + NodeValueArray output; - for (int i=0; ifirst] = GenerateRowValueElement(node, input, it->first, &it->second, time); } value = NodeValue(value.type(), QVariant::fromValue(output), value.source(), value.array(), value.tag()); @@ -179,9 +179,9 @@ void NodeTraverser::Transform(QTransform *transform, const Node *start, const No transform_ = nullptr; } -NodeGlobals NodeTraverser::GenerateGlobals(const VideoParams ¶ms, const TimeRange &time) +NodeGlobals NodeTraverser::GenerateGlobals(const VideoParams &vparams, const AudioParams &aparams, const TimeRange &time) { - return NodeGlobals(params, time); + return NodeGlobals(vparams, aparams, time); } NodeValueTable NodeTraverser::ProcessInput(const Node* node, const QString& input, const TimeRange& range) @@ -205,18 +205,17 @@ NodeValueTable NodeTraverser::ProcessInput(const Node* node, const QString& inpu if (is_array) { // Value is an array, we will return a list of NodeValueTables - QVector array_tbl(node->InputArraySize(input)); + NodeValueTableArray array_tbl; - for (int i=0; iInputTimeAdjustment(input, i, range); - - if (node->IsInputConnected(input, i)) { - Node *output = node->GetConnectedOutput(input, i); - sub_tbl = GenerateTable(output, adjusted_range, node); - } else { - QVariant input_value = node->GetValueAtTime(input, adjusted_range.in(), i); - sub_tbl.Push(node->GetInputDataType(input), input_value, node); + Node::ActiveElements a = node->GetActiveElementsAtTime(input, range); + if (a.mode() == Node::ActiveElements::kAllElements) { + int sz = node->InputArraySize(input); + for (int i=0; iInputTimeAdjustment(input, element, range); + + if (node->IsInputConnected(input, element)) { + Node *output = node->GetConnectedOutput(input, element); + sub_tbl = GenerateTable(output, adjusted_range, node); + } else { + QVariant input_value = node->GetValueAtTime(input, adjusted_range.in(), element); + sub_tbl.Push(node->GetInputDataType(input), input_value, node); + } +} + NodeTraverser::NodeTraverser() : cancel_(nullptr), transform_(nullptr), @@ -262,12 +275,6 @@ NodeValueTable NodeTraverser::GenerateTable(const Node *n, const TimeRange& rang // NOTE: Times how long a node takes to process, useful for profiling. //GTTTime gtt(n);Q_UNUSED(gtt); - const Track* track = dynamic_cast(n); - if (track) { - // If the range is not wholly contained in this Block, we'll need to do some extra processing - return GenerateBlockTable(track, range); - } - // Use table cache to skip processing where available if (value_cache_.contains(n)) { QHash &node_value_map = value_cache_[n]; @@ -297,7 +304,7 @@ NodeValueTable NodeTraverser::GenerateTable(const Node *n, const TimeRange& rang table = database.Merge(); // By this point, the node should have all the inputs it needs to render correctly - NodeGlobals globals = GenerateGlobals(video_params_, range); + NodeGlobals globals = GenerateGlobals(video_params_, audio_params_, range); n->Value(row, globals, &table); // `transform_now_` is the next node in the path that needs to be traversed. It only ever goes @@ -331,22 +338,6 @@ NodeValueTable NodeTraverser::GenerateTable(const Node *n, const TimeRange& rang return table; } -NodeValueTable NodeTraverser::GenerateBlockTable(const Track *track, const TimeRange &range) -{ - // By default, just follow the in point - Block* active_block = track->BlockAtTime(range.in()); - - NodeValueTable table; - - if (active_block) { - block_stack_.push_back(active_block); - table = GenerateTable(active_block, Track::TransformRangeForBlock(active_block, range), track); - block_stack_.pop_back(); - } - - return table; -} - TexturePtr NodeTraverser::ProcessVideoCacheJob(const CacheJob *val) { return nullptr; diff --git a/app/node/traverser.h b/app/node/traverser.h index e008c1f85..4f2041d57 100644 --- a/app/node/traverser.h +++ b/app/node/traverser.h @@ -42,7 +42,7 @@ public: NodeValueTable GenerateTable(const Node *n, const TimeRange &range, const Node *next_node = nullptr); - NodeValueDatabase GenerateDatabase(const Node *node, const TimeRange &range); + virtual NodeValueDatabase GenerateDatabase(const Node *node, const TimeRange &range); NodeValueRow GenerateRow(NodeValueDatabase *database, const Node *node, const TimeRange &range); NodeValueRow GenerateRow(const Node *node, const TimeRange &range); @@ -54,10 +54,10 @@ public: void Transform(QTransform *transform, const Node *start, const Node *end, const TimeRange &range); - static NodeGlobals GenerateGlobals(const VideoParams ¶ms, const TimeRange &time); - static NodeGlobals GenerateGlobals(const VideoParams ¶ms, const rational &time) + static NodeGlobals GenerateGlobals(const VideoParams &vparams, const AudioParams &aparams, const TimeRange &time); + static NodeGlobals GenerateGlobals(const VideoParams &vparams, const AudioParams &aparams, const rational &time) { - return GenerateGlobals(params, TimeRange(time, time + params.frame_rate_as_time_base())); + return GenerateGlobals(vparams, aparams, TimeRange(time, time + vparams.frame_rate_as_time_base())); } const VideoParams& GetCacheVideoParams() const @@ -83,7 +83,7 @@ public: protected: NodeValueTable ProcessInput(const Node *node, const QString &input, const TimeRange &range); - virtual NodeValueTable GenerateBlockTable(const Track *track, const TimeRange& range); + void ProcessInputElement(NodeValueTableArray &array_tbl, const Node *node, const QString &input, int element, const TimeRange &range); virtual void ProcessVideoFootage(TexturePtr destination, const FootageJob *stream, const rational &input_time){} diff --git a/app/node/value.h b/app/node/value.h index e90381069..3a3f75501 100644 --- a/app/node/value.h +++ b/app/node/value.h @@ -31,11 +31,15 @@ #include "node/splitvalue.h" #include "render/color.h" #include "render/texture.h" -#include "undo/undocommand.h" namespace olive { class Node; +class NodeValue; +class NodeValueTable; + +using NodeValueArray = std::map; +using NodeValueTableArray = std::map; class NodeValue { @@ -344,7 +348,7 @@ public: QVector3D toVec3() const { return value(); } QVector4D toVec4() const { return value(); } Bezier toBezier() const { return value(); } - QVector toArray() const { return value >(); } + NodeValueArray toArray() const { return value(); } private: Type type_; diff --git a/app/panel/CMakeLists.txt b/app/panel/CMakeLists.txt index e13af427d..6bfa5a43b 100644 --- a/app/panel/CMakeLists.txt +++ b/app/panel/CMakeLists.txt @@ -17,6 +17,7 @@ add_subdirectory(audiomonitor) add_subdirectory(curve) add_subdirectory(footageviewer) +add_subdirectory(multicam) add_subdirectory(node) add_subdirectory(param) add_subdirectory(pixelsampler) diff --git a/app/panel/multicam/CMakeLists.txt b/app/panel/multicam/CMakeLists.txt new file mode 100644 index 000000000..89062f21e --- /dev/null +++ b/app/panel/multicam/CMakeLists.txt @@ -0,0 +1,22 @@ +# Olive - Non-Linear Video Editor +# Copyright (C) 2022 Olive Team +# +# 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 . + +set(OLIVE_SOURCES + ${OLIVE_SOURCES} + panel/multicam/multicampanel.h + panel/multicam/multicampanel.cpp + PARENT_SCOPE +) diff --git a/app/panel/multicam/multicampanel.cpp b/app/panel/multicam/multicampanel.cpp new file mode 100644 index 000000000..7218e43e3 --- /dev/null +++ b/app/panel/multicam/multicampanel.cpp @@ -0,0 +1,22 @@ +#include "multicampanel.h" + +namespace olive { + +#define super TimeBasedPanel + +MulticamPanel::MulticamPanel(QWidget *parent) : + super(QStringLiteral("MultiCamPanel"), parent) +{ + SetTimeBasedWidget(new MulticamWidget()); + + Retranslate(); +} + +void MulticamPanel::Retranslate() +{ + super::Retranslate(); + + SetTitle(tr("Multi-Cam")); +} + +} diff --git a/app/panel/multicam/multicampanel.h b/app/panel/multicam/multicampanel.h new file mode 100644 index 000000000..1b6dee6fc --- /dev/null +++ b/app/panel/multicam/multicampanel.h @@ -0,0 +1,24 @@ +#ifndef MULTICAMPANEL_H +#define MULTICAMPANEL_H + +#include "panel/viewer/viewerbase.h" +#include "widget/multicam/multicamwidget.h" + +namespace olive { + +class MulticamPanel : public TimeBasedPanel +{ + Q_OBJECT +public: + MulticamPanel(QWidget* parent = nullptr); + + MulticamWidget *GetMulticamWidget() const { return static_cast(GetTimeBasedWidget()); } + +protected: + virtual void Retranslate() override; + +}; + +} + +#endif // MULTICAMPANEL_H diff --git a/app/panel/timebased/timebased.h b/app/panel/timebased/timebased.h index dff33ccca..4101e6b09 100644 --- a/app/panel/timebased/timebased.h +++ b/app/panel/timebased/timebased.h @@ -106,6 +106,8 @@ public: virtual void Paste() override; + TimeBasedWidget* GetTimeBasedWidget() const { return widget_; } + public slots: void SetTimebase(const rational& timebase); @@ -127,11 +129,6 @@ signals: void ShuttleRightRequested(); protected: - TimeBasedWidget* GetTimeBasedWidget() const - { - return widget_; - } - void SetTimeBasedWidget(TimeBasedWidget* widget); virtual void Retranslate() override; diff --git a/app/panel/timeline/timeline.h b/app/panel/timeline/timeline.h index 611fd275c..64868354e 100644 --- a/app/panel/timeline/timeline.h +++ b/app/panel/timeline/timeline.h @@ -98,6 +98,11 @@ public: timeline_widget()->ShowSpeedDurationDialogForSelectedClips(); } + void NestSelectedClips() + { + timeline_widget()->NestSelectedClips(); + } + void InsertFootageAtPlayhead(const QVector &footage); void OverwriteFootageAtPlayhead(const QVector &footage); @@ -107,6 +112,11 @@ public: return timeline_widget()->GetSelectedBlocks(); } + Sequence *GetSequence() const + { + return dynamic_cast(GetConnectedViewer()); + } + protected: virtual void Retranslate() override; diff --git a/app/panel/viewer/viewerbase.cpp b/app/panel/viewer/viewerbase.cpp index 6c90506b6..c1aac4d13 100644 --- a/app/panel/viewer/viewerbase.cpp +++ b/app/panel/viewer/viewerbase.cpp @@ -24,35 +24,37 @@ namespace olive { +#define super TimeBasedPanel + ViewerPanelBase::ViewerPanelBase(const QString& object_name, QWidget *parent) : - TimeBasedPanel(object_name, parent) + super(object_name, parent) { connect(PanelManager::instance(), &PanelManager::FocusedPanelChanged, this, &ViewerPanelBase::FocusedPanelChanged); } void ViewerPanelBase::PlayPause() { - static_cast(GetTimeBasedWidget())->TogglePlayPause(); + GetViewerWidget()->TogglePlayPause(); } void ViewerPanelBase::PlayInToOut() { - static_cast(GetTimeBasedWidget())->Play(true); + GetViewerWidget()->Play(true); } void ViewerPanelBase::ShuttleLeft() { - static_cast(GetTimeBasedWidget())->ShuttleLeft(); + GetViewerWidget()->ShuttleLeft(); } void ViewerPanelBase::ShuttleStop() { - static_cast(GetTimeBasedWidget())->ShuttleStop(); + GetViewerWidget()->ShuttleStop(); } void ViewerPanelBase::ShuttleRight() { - static_cast(GetTimeBasedWidget())->ShuttleRight(); + GetViewerWidget()->ShuttleRight(); } void ViewerPanelBase::ConnectTimeBasedPanel(TimeBasedPanel *panel) @@ -75,22 +77,22 @@ void ViewerPanelBase::DisconnectTimeBasedPanel(TimeBasedPanel *panel) void ViewerPanelBase::SetFullScreen(QScreen *screen) { - static_cast(GetTimeBasedWidget())->SetFullScreen(screen); + GetViewerWidget()->SetFullScreen(screen); } void ViewerPanelBase::SetGizmos(Node *node) { - static_cast(GetTimeBasedWidget())->SetGizmos(node); + GetViewerWidget()->SetGizmos(node); } void ViewerPanelBase::CacheEntireSequence() { - static_cast(GetTimeBasedWidget())->CacheEntireSequence(); + GetViewerWidget()->CacheEntireSequence(); } void ViewerPanelBase::CacheSequenceInOut() { - static_cast(GetTimeBasedWidget())->CacheSequenceInOut(); + GetViewerWidget()->CacheSequenceInOut(); } void ViewerPanelBase::SetViewerWidget(ViewerWidget *vw) @@ -104,9 +106,11 @@ void ViewerPanelBase::SetViewerWidget(ViewerWidget *vw) void ViewerPanelBase::FocusedPanelChanged(PanelWidget *panel) { - auto vw = static_cast(GetTimeBasedWidget()); - if (vw->IsPlaying() && panel != this) { - vw->Pause(); + if (dynamic_cast(panel)) { + auto vw = GetViewerWidget(); + if (vw->IsPlaying() && panel != this) { + vw->Pause(); + } } } diff --git a/app/panel/viewer/viewerbase.h b/app/panel/viewer/viewerbase.h index 1ca7c06ff..16c733b35 100644 --- a/app/panel/viewer/viewerbase.h +++ b/app/panel/viewer/viewerbase.h @@ -33,6 +33,11 @@ class ViewerPanelBase : public TimeBasedPanel public: ViewerPanelBase(const QString& object_name, QWidget* parent = nullptr); + ViewerWidget *GetViewerWidget() const + { + return static_cast(GetTimeBasedWidget()); + } + virtual void PlayPause() override; virtual void PlayInToOut() override; @@ -54,12 +59,32 @@ public: ColorManager *GetColorManager() { - return static_cast(GetTimeBasedWidget())->color_manager(); + return GetViewerWidget()->color_manager(); } void UpdateTextureFromNode() { - static_cast(GetTimeBasedWidget())->UpdateTextureFromNode(); + GetViewerWidget()->UpdateTextureFromNode(); + } + + void AddPlaybackDevice(ViewerDisplayWidget *vw) + { + GetViewerWidget()->AddPlaybackDevice(vw); + } + + void SetTimelineSelectedBlocks(const QVector &b) + { + GetViewerWidget()->SetTimelineSelectedBlocks(b); + } + + void SetNodeViewSelections(const QVector &n) + { + GetViewerWidget()->SetNodeViewSelections(n); + } + + void ConnectMulticamWidget(MulticamWidget *p) + { + GetViewerWidget()->ConnectMulticamWidget(p); } public slots: @@ -71,7 +96,7 @@ public slots: void RequestStartEditingText() { - static_cast(GetTimeBasedWidget())->RequestStartEditingText(); + GetViewerWidget()->RequestStartEditingText(); } signals: diff --git a/app/render/audiowaveformcache.cpp b/app/render/audiowaveformcache.cpp index 5ed1ee280..4f9870759 100644 --- a/app/render/audiowaveformcache.cpp +++ b/app/render/audiowaveformcache.cpp @@ -22,113 +22,94 @@ namespace olive { +#define super PlaybackCache + AudioWaveformCache::AudioWaveformCache(QObject *parent) : - PlaybackCache{parent} + super{parent} { + waveforms_ = std::make_shared(); } void AudioWaveformCache::WriteWaveform(const TimeRange &range, const TimeRangeList &valid_ranges, const AudioVisualWaveform *waveform) { // Write each valid range to the segments foreach (const TimeRange& r, valid_ranges) { -#ifdef AVW_USE_LIST - // Write visual - TimeRangeList::util_remove(&waveforms_, r); - if (waveform) { - TimeRangeWithWaveform wv = r; - rational local_start = r.in() - range.in(); - if (local_start != 0) { - wv.waveform = waveform->Mid(local_start, r.length()); - } else { - wv.waveform = *waveform; - } - waveforms_.append(wv); + waveforms_->OverwriteSums(*waveform, r.in(), r.in() - range.in(), r.length()); } -#else - if (waveform) { - waveforms_.OverwriteSums(*waveform, r.in(), r.in() - range.in(), r.length()); - } -#endif Validate(r); } } +void DrawSubRect(QPainter *painter, const QRect &rect, const double &scale, const TimeRange &wave_range, const AudioVisualWaveform &waveform, const TimeRange &subrange) +{ + // Find start time of passthrough + TimeRange intersect = wave_range.Intersected(subrange); + + // Create new rect that starts at the offset of pass_start from start_time + // Set rect width to either length of passthrough or until the end + QRect pass_rect(rect.x() + (intersect.in() - wave_range.in()).toDouble() * scale, + rect.y(), + intersect.length().toDouble() * scale, + rect.height()); + + // Draw waveform with this info + AudioVisualWaveform::DrawWaveform(painter, pass_rect, scale, waveform, intersect.in()); +} + void AudioWaveformCache::Draw(QPainter *painter, const QRect &rect, const double &scale, const rational &start_time) const { - rational end = start_time + rational::fromDouble(rect.width() / scale); - TimeRange draw_range(start_time, end); + if (!passthroughs_.empty()) { + TimeRange wave_range(start_time, start_time + rational::fromDouble(rect.width() / scale)); + TimeRangeList draw_range = {wave_range}; + for (const WaveformPassthrough &p : passthroughs_) { + if (draw_range.OverlapsWith(p, true, false)) { + DrawSubRect(painter, rect, scale, wave_range, *p.waveform, p); -#ifdef AVW_USE_LIST - foreach (const TimeRangeWithWaveform &wv, waveforms_) { - if (wv.OverlapsWith(draw_range)) { - rational substart = std::max(wv.in(), draw_range.in()); - rational subend = std::min(wv.out(), draw_range.out()); - - QRect subrect = rect; - subrect.setLeft(subrect.left() + (substart - draw_range.in()).toDouble()*scale); - subrect.setWidth((subend - substart).toDouble()*scale); - - rational local_start = substart - wv.in(); - AudioVisualWaveform::DrawWaveform(painter, subrect, scale, wv.waveform, local_start); + // Remove this range + draw_range.remove(p); + } } + + for (const TimeRange &r : draw_range) { + DrawSubRect(painter, rect, scale, wave_range, *waveforms_, r); + } + } else { + AudioVisualWaveform::DrawWaveform(painter, rect, scale, *waveforms_, start_time); } -#else - AudioVisualWaveform::DrawWaveform(painter, rect, scale, waveforms_, start_time); -#endif } AudioVisualWaveform::Sample AudioWaveformCache::GetSummaryFromTime(const rational &start, const rational &length) const { -#ifdef AVW_USE_LIST - QMap sample; - - TimeRange acquire(start, start+length); - foreach (const TimeRangeWithWaveform &wv, waveforms_) { - if (wv.OverlapsWith(acquire)) { - TimeRange this_range = wv.Intersected(acquire); - auto sum = wv.waveform.GetSummaryFromTime(this_range.in() - wv.in(), this_range.length()); - sample.insert(this_range.in(), sum); - } - } - - AudioVisualWaveform::Sample result; - - for (auto it=sample.cbegin(); it!=sample.cend(); it++) { - result.insert(result.end(), it.value().begin(), it.value().end()); - } - - return result; -#else - return waveforms_.GetSummaryFromTime(start, length); -#endif + return waveforms_->GetSummaryFromTime(start, length); } rational AudioWaveformCache::length() const { -#ifdef AVW_USE_LIST - rational len = 0; - - foreach (const TimeRangeWithWaveform &wv, waveforms_) { - len = std::max(len, wv.out()); - } - - return len; -#else - return waveforms_.length(); -#endif + return waveforms_->length(); } void AudioWaveformCache::SetPassthrough(PlaybackCache *cache) { AudioWaveformCache *c = static_cast(cache); - waveforms_ = c->waveforms_; + for (const TimeRange &r : c->GetValidatedRanges()) { - Validate(r); + WaveformPassthrough t = r; + t.waveform = c->waveforms_; + passthroughs_.append(t); } + passthroughs_.append(c->passthroughs_); + SetParameters(c->GetParameters()); SetSavingEnabled(c->IsSavingEnabled()); } +void AudioWaveformCache::InvalidateEvent(const TimeRange& range) +{ + TimeRangeList::util_remove(&passthroughs_, range); + + super::InvalidateEvent(range); +} + } diff --git a/app/render/audiowaveformcache.h b/app/render/audiowaveformcache.h index a1a6dcfe5..95a498d4f 100644 --- a/app/render/audiowaveformcache.h +++ b/app/render/audiowaveformcache.h @@ -24,8 +24,6 @@ #include "audio/audiovisualwaveform.h" #include "playbackcache.h" -//#define AVW_USE_LIST - namespace olive { class AudioWaveformCache : public PlaybackCache @@ -40,7 +38,7 @@ public: void SetParameters(const AudioParams &p) { params_ = p; - waveforms_.set_channel_count(p.channel_count()); + waveforms_->set_channel_count(p.channel_count()); } void Draw(QPainter* painter, const QRect &rect, const double &scale, const rational &start_time) const; @@ -51,45 +49,28 @@ public: virtual void SetPassthrough(PlaybackCache *cache) override; +protected: + virtual void InvalidateEvent(const TimeRange& range) override; + private: -#ifdef AVW_USE_LIST - class TimeRangeWithWaveform : public TimeRange - { - public: - TimeRangeWithWaveform() = default; - TimeRangeWithWaveform(const TimeRange &r) : - TimeRange(r) - { - } + using WaveformPtr = std::shared_ptr; - void set_in(const rational& in) - { - waveform.TrimIn(in - this->in()); - TimeRange::set_in(in); - } - - void set_out(const rational& out) - { - waveform.Resize(out - this->in()); - TimeRange::set_out(out); - } - - void set_range(const rational& in, const rational& out) - { - waveform.TrimRange(in, out-in); - TimeRange::set_range(in, out); - } - - AudioVisualWaveform waveform; - }; - - QVector waveforms_; -#else - AudioVisualWaveform waveforms_; -#endif + WaveformPtr waveforms_; AudioParams params_; + class WaveformPassthrough : public TimeRange + { + public: + WaveformPassthrough(const TimeRange &r) : + TimeRange(r) + {} + + WaveformPtr waveform; + }; + + QVector passthroughs_; + }; } diff --git a/app/render/playbackcache.cpp b/app/render/playbackcache.cpp index bf606e153..7cc89d473 100644 --- a/app/render/playbackcache.cpp +++ b/app/render/playbackcache.cpp @@ -223,6 +223,13 @@ void PlaybackCache::InvalidateAll() Invalidate(TimeRange(0, RATIONAL_MAX)); } +void PlaybackCache::Request(const TimeRange &r) +{ + requested_.insert(r); + + emit Requested(r); +} + void PlaybackCache::Validate(const TimeRange &r, bool signal) { validated_.insert(r); diff --git a/app/render/playbackcache.h b/app/render/playbackcache.h index 3485fdcb4..8f483d1c5 100644 --- a/app/render/playbackcache.h +++ b/app/render/playbackcache.h @@ -98,15 +98,29 @@ public: const QVector &GetPassthroughs() const { return passthroughs_; } + void ClearRequestRange(const olive::TimeRange &r) + { + requested_.remove(r); + } + + void ResignalRequests() + { + for (const TimeRange &r : requested_) { + emit Requested(r); + } + } + public slots: void InvalidateAll(); + void Request(const olive::TimeRange &r); + signals: void Invalidated(const olive::TimeRange& r); void Validated(const olive::TimeRange& r); - void Request(const olive::TimeRange& r); + void Requested(const olive::TimeRange& r); void CancelAll(); @@ -124,6 +138,8 @@ protected: private: TimeRangeList validated_; + TimeRangeList requested_; + QUuid uuid_; bool saving_enabled_; diff --git a/app/render/previewautocacher.cpp b/app/render/previewautocacher.cpp index 5a9a5a21e..59ff3d3c9 100644 --- a/app/render/previewautocacher.cpp +++ b/app/render/previewautocacher.cpp @@ -24,6 +24,7 @@ #include #include "codec/conformmanager.h" +#include "node/input/multicam/multicamnode.h" #include "node/inputdragger.h" #include "node/project/project.h" #include "render/diskmanager.h" @@ -41,7 +42,9 @@ PreviewAutoCacher::PreviewAutoCacher(QObject *parent) : use_custom_range_(false), pause_renders_(false), single_frame_render_(nullptr), - display_color_processor_(nullptr) + display_color_processor_(nullptr), + multicam_(nullptr), + ignore_cache_requests_(false) { // Set defaults SetPlayhead(0); @@ -62,6 +65,11 @@ PreviewAutoCacher::~PreviewAutoCacher() } RenderTicketPtr PreviewAutoCacher::GetSingleFrame(const rational &t, bool dry) +{ + return GetSingleFrame(viewer_node_->GetConnectedTextureOutput(), t, dry); +} + +RenderTicketPtr PreviewAutoCacher::GetSingleFrame(Node *n, const rational &t, bool dry) { // If we have a single frame render queued (but not yet sent to the RenderManager), cancel it now CancelQueuedSingleFrameRender(); @@ -71,6 +79,7 @@ RenderTicketPtr PreviewAutoCacher::GetSingleFrame(const rational &t, bool dry) sfr->Start(); sfr->setProperty("time", QVariant::fromValue(t)); sfr->setProperty("dry", dry); + sfr->setProperty("node", Node::PtrToValue(n)); // Queue it and try to render single_frame_render_ = sfr; @@ -100,6 +109,8 @@ void PreviewAutoCacher::VideoInvalidatedFromCache(const TimeRange &range) { PlaybackCache *cache = static_cast(sender()); + cache->ClearRequestRange(range); + VideoInvalidatedFromNode(cache, range); } @@ -107,6 +118,8 @@ void PreviewAutoCacher::AudioInvalidatedFromCache(const TimeRange &range) { PlaybackCache *cache = static_cast(sender()); + cache->ClearRequestRange(range); + AudioInvalidatedFromNode(cache, range); } @@ -207,6 +220,7 @@ void PreviewAutoCacher::VideoRendered() QVector tickets = video_immediate_passthroughs_.take(watcher); foreach (RenderTicketPtr t, tickets) { if (watcher->HasResult()) { + t->setProperty("multicam_output", watcher->GetTicket()->property("multicam_output")); t->Finish(watcher->Get()); } else { t->Finish(); @@ -240,7 +254,10 @@ void PreviewAutoCacher::VideoRendered() void PreviewAutoCacher::ProcessUpdateQueue() { // Iterate everything that happened to the graph and do the same thing on our end - foreach (const QueuedJob& job, graph_update_queue_) { + while (!graph_update_queue_.empty()) { + QueuedJob job = graph_update_queue_.front(); + graph_update_queue_.pop_front(); + switch (job.type) { case QueuedJob::kNodeAdded: AddNode(job.node); @@ -262,7 +279,6 @@ void PreviewAutoCacher::ProcessUpdateQueue() break; } } - graph_update_queue_.clear(); // Indicate that we have synchronized to this point, which is compared with the graph change // time to see if our copied graph is up to date @@ -362,28 +378,30 @@ void PreviewAutoCacher::InsertIntoCopyMap(Node *node, Node *copy) Node::CopyInputs(node, copy, false); // Connect to node's cache - ConnectToNodeCache(node); + if (!ignore_cache_requests_) { + ConnectToNodeCache(node); + } } void PreviewAutoCacher::ConnectToNodeCache(Node *node) { connect(node->video_frame_cache(), - &PlaybackCache::Request, + &PlaybackCache::Requested, this, &PreviewAutoCacher::VideoInvalidatedFromCache); connect(node->thumbnail_cache(), - &PlaybackCache::Request, + &PlaybackCache::Requested, this, &PreviewAutoCacher::VideoInvalidatedFromCache); connect(node->audio_playback_cache(), - &PlaybackCache::Request, + &PlaybackCache::Requested, this, &PreviewAutoCacher::AudioInvalidatedFromCache); connect(node->waveform_cache(), - &PlaybackCache::Request, + &PlaybackCache::Requested, this, &PreviewAutoCacher::AudioInvalidatedFromCache); @@ -396,27 +414,32 @@ void PreviewAutoCacher::ConnectToNodeCache(Node *node) &PlaybackCache::CancelAll, this, &PreviewAutoCacher::CancelForCache); + + node->video_frame_cache()->ResignalRequests(); + node->thumbnail_cache()->ResignalRequests(); + node->audio_playback_cache()->ResignalRequests(); + node->waveform_cache()->ResignalRequests(); } void PreviewAutoCacher::DisconnectFromNodeCache(Node *node) { disconnect(node->video_frame_cache(), - &PlaybackCache::Request, + &PlaybackCache::Requested, this, &PreviewAutoCacher::VideoInvalidatedFromCache); disconnect(node->thumbnail_cache(), - &PlaybackCache::Request, + &PlaybackCache::Requested, this, &PreviewAutoCacher::VideoInvalidatedFromCache); disconnect(node->audio_playback_cache(), - &PlaybackCache::Request, + &PlaybackCache::Requested, this, &PreviewAutoCacher::AudioInvalidatedFromCache); disconnect(node->waveform_cache(), - &PlaybackCache::Request, + &PlaybackCache::Requested, this, &PreviewAutoCacher::AudioInvalidatedFromCache); @@ -466,6 +489,8 @@ void PreviewAutoCacher::StartCachingVideoRange(PlaybackCache *cache, const TimeR using_tb = viewer_node_->GetVideoParams().frame_rate_as_time_base(); } + cache->ClearRequestRange(range); + TimeRangeListFrameIterator iterator({range}, using_tb); pending_video_jobs_.push_back({node, cache, range, iterator}); video_cache_data_[cache].job_tracker.insert(TimeRange(iterator.Snap(range.in()), range.out()), graph_changed_time_); @@ -475,6 +500,9 @@ void PreviewAutoCacher::StartCachingVideoRange(PlaybackCache *cache, const TimeR void PreviewAutoCacher::StartCachingAudioRange(PlaybackCache *cache, const TimeRange &range) { Node *node = cache->parent(); + + cache->ClearRequestRange(range); + pending_audio_jobs_.push_back({node, cache, range}); audio_cache_data_[cache].job_tracker.insert(range, graph_changed_time_); TryRender(); @@ -486,6 +514,8 @@ void PreviewAutoCacher::VideoInvalidatedFromNode(PlaybackCache *cache, const Tim // want to dedicate all our rendering power to realtime feedback for the user //CancelVideoTasks(node); + cache->ClearRequestRange(range); + // If auto-cache is enabled and a slider is not being dragged, queue up to hash these frames if (!NodeInputDragger::IsInputBeingDragged()) { StartCachingVideoRange(cache, range); @@ -498,6 +528,8 @@ void PreviewAutoCacher::AudioInvalidatedFromNode(PlaybackCache *cache, const Tim // cancelled, so some areas may end up unrendered forever // ClearAudioQueue(); + cache->ClearRequestRange(range); + // If we're auto-caching audio or require realtime waveforms, we'll have to render this StartCachingAudioRange(cache, range); } @@ -553,37 +585,37 @@ void PreviewAutoCacher::SetRendersPaused(bool e) void PreviewAutoCacher::NodeAdded(Node *node) { - graph_update_queue_.append({QueuedJob::kNodeAdded, node, NodeInput(), nullptr}); + graph_update_queue_.push_back({QueuedJob::kNodeAdded, node, NodeInput(), nullptr}); UpdateGraphChangeValue(); } void PreviewAutoCacher::NodeRemoved(Node *node) { - graph_update_queue_.append({QueuedJob::kNodeRemoved, node, NodeInput(), nullptr}); + graph_update_queue_.push_back({QueuedJob::kNodeRemoved, node, NodeInput(), nullptr}); UpdateGraphChangeValue(); } void PreviewAutoCacher::EdgeAdded(Node *output, const NodeInput &input) { - graph_update_queue_.append({QueuedJob::kEdgeAdded, nullptr, input, output}); + graph_update_queue_.push_back({QueuedJob::kEdgeAdded, nullptr, input, output}); UpdateGraphChangeValue(); } void PreviewAutoCacher::EdgeRemoved(Node *output, const NodeInput &input) { - graph_update_queue_.append({QueuedJob::kEdgeRemoved, nullptr, input, output}); + graph_update_queue_.push_back({QueuedJob::kEdgeRemoved, nullptr, input, output}); UpdateGraphChangeValue(); } void PreviewAutoCacher::ValueChanged(const NodeInput &input) { - graph_update_queue_.append({QueuedJob::kValueChanged, nullptr, input, nullptr}); + graph_update_queue_.push_back({QueuedJob::kValueChanged, nullptr, input, nullptr}); UpdateGraphChangeValue(); } void PreviewAutoCacher::ValueHintChanged(const NodeInput &input) { - graph_update_queue_.append({QueuedJob::kValueHintChanged, nullptr, input, nullptr}); + graph_update_queue_.push_back({QueuedJob::kValueHintChanged, nullptr, input, nullptr}); UpdateGraphChangeValue(); } @@ -591,7 +623,7 @@ void PreviewAutoCacher::TryRender() { delayed_requeue_timer_.stop(); - if (!graph_update_queue_.isEmpty()) { + if (!graph_update_queue_.empty()) { // Check if we have jobs running in other threads that shouldn't be interrupted right now // NOTE: We don't check for downloads because, while they run in another thread, they don't // require any access to the graph and therefore don't risk race conditions. @@ -611,11 +643,19 @@ void PreviewAutoCacher::TryRender() single_frame_render_ = nullptr; // Check if already caching this - RenderTicketWatcher *watcher = RenderFrame(copied_viewer_node_->GetConnectedTextureOutput(), - t->property("time").value(), - nullptr, - t->property("dry").toBool()); - video_immediate_passthroughs_[watcher].append(t); + Node *n = Node::ValueToPtr(t->property("node")); + Node *copy = copy_map_.value(n); + + if (copy) { + RenderTicketWatcher *watcher = RenderFrame(copy, + t->property("time").value(), + nullptr, + t->property("dry").toBool()); + video_immediate_passthroughs_[watcher].append(t); + } else { + qWarning() << "Failed to find copied node for SFR ticket"; + t->Finish(); + } } if (!pause_renders_) { @@ -716,6 +756,9 @@ RenderTicketWatcher* PreviewAutoCacher::RenderFrame(Node *node, const rational& // Allow using cached images for this render job rvp.use_cache = true; + // Multicam + rvp.multicam = static_cast(copy_map_.value(multicam_)); + watcher->SetTicket(RenderManager::instance()->RenderFrame(rvp)); return watcher; @@ -822,6 +865,9 @@ void PreviewAutoCacher::SetViewerNode(ViewerOutput *viewer_node) video_cache_data_.clear(); audio_cache_data_.clear(); + // Clear multicam reference + multicam_ = nullptr; + // Disconnect signals for future node additions/deletions NodeGraph* graph = viewer_node_->parent(); diff --git a/app/render/previewautocacher.h b/app/render/previewautocacher.h index 05e253851..28c16deb7 100644 --- a/app/render/previewautocacher.h +++ b/app/render/previewautocacher.h @@ -50,6 +50,7 @@ public: virtual ~PreviewAutoCacher() override; RenderTicketPtr GetSingleFrame(const rational& t, bool dry = false); + RenderTicketPtr GetSingleFrame(Node *n, const rational& t, bool dry = false); RenderTicketPtr GetRangeOfAudio(TimeRange range); @@ -89,6 +90,10 @@ public: void SetRendersPaused(bool e); + void SetMulticamNode(MultiCamNode *n) { multicam_ = n; } + + void SetIgnoreCacheRequests(bool e) { ignore_cache_requests_ = e; } + public slots: void SetDisplayColorProcessor(ColorProcessorPtr processor) { @@ -160,7 +165,7 @@ private: Project copied_project_; - QVector graph_update_queue_; + std::list graph_update_queue_; QHash copy_map_; QHash graph_map_; ViewerOutput* copied_viewer_node_; @@ -217,6 +222,10 @@ private: ColorProcessorPtr display_color_processor_; + MultiCamNode *multicam_; + + bool ignore_cache_requests_; + private slots: /** * @brief Handler for when the NodeGraph reports a video change over a certain time range diff --git a/app/render/rendermanager.cpp b/app/render/rendermanager.cpp index 19a0d8efc..32b1a9574 100644 --- a/app/render/rendermanager.cpp +++ b/app/render/rendermanager.cpp @@ -55,7 +55,11 @@ RenderManager::RenderManager(QObject *parent) : video_thread_ = CreateThread(context_); dry_run_thread_ = CreateThread(); audio_thread_ = CreateThread(); - waveform_thread_ = CreateThread(); + + waveform_threads_.resize(QThread::idealThreadCount()); + for (size_t i=0; isetProperty("cache", params.cache_dir); ticket->setProperty("cachetimebase", QVariant::fromValue(params.cache_timebase)); ticket->setProperty("cacheid", QVariant::fromValue(params.cache_id)); + ticket->setProperty("multicam", Node::PtrToValue(params.multicam)); if (params.return_type == ReturnType::kNull) { dry_run_thread_->AddTicket(ticket); @@ -135,7 +140,10 @@ RenderTicketPtr RenderManager::RenderAudio(const RenderAudioParams ¶ms) ticket->setProperty("mode", params.mode); if (params.generate_waveforms) { - waveform_thread_->AddTicket(ticket); + size_t thread_index = last_waveform_thread_%waveform_threads_.size(); + RenderThread *thread = waveform_threads_[thread_index]; + thread->AddTicket(ticket); + last_waveform_thread_++; } else { audio_thread_->AddTicket(ticket); } diff --git a/app/render/rendermanager.h b/app/render/rendermanager.h index b33818b58..25dbae894 100644 --- a/app/render/rendermanager.h +++ b/app/render/rendermanager.h @@ -117,6 +117,7 @@ public: force_size = QSize(0, 0); force_channel_count = 0; mode = m; + multicam = nullptr; } void AddCache(FrameHashCache *cache) @@ -134,6 +135,7 @@ public: bool use_cache; ReturnType return_type; RenderMode::Mode mode; + MultiCamNode *multicam; QString cache_dir; rational cache_timebase; @@ -230,7 +232,9 @@ private: RenderThread *video_thread_; RenderThread *dry_run_thread_; RenderThread *audio_thread_; - RenderThread *waveform_thread_; + + std::vector waveform_threads_; + size_t last_waveform_thread_; std::list render_threads_; diff --git a/app/render/renderprocessor.cpp b/app/render/renderprocessor.cpp index 0327c1e9e..2b42f9e67 100644 --- a/app/render/renderprocessor.cpp +++ b/app/render/renderprocessor.cpp @@ -294,118 +294,36 @@ DecoderPtr RenderProcessor::ResolveDecoderFromInput(const QString& decoder_id, c return dec; } +NodeValueDatabase RenderProcessor::GenerateDatabase(const Node *node, const TimeRange &range) +{ + NodeValueDatabase db = super::GenerateDatabase(node, range); + + if (const MultiCamNode *multicam = dynamic_cast(node)) { + if (Node::ValueToPtr(ticket_->property("multicam")) == multicam) { + int sz = multicam->InputArraySize(multicam->kSourcesInput); + NodeValueTableArray arr; + QVector multicam_tex(sz); + for (int i=0; ikSourcesInput, i, range); + + NodeValue val = GenerateRowValueElement(multicam, multicam->kSourcesInput, i, &arr.at(i), range); + ResolveJobs(val); + + multicam_tex[i] = val.toTexture(); + } + ticket_->setProperty("multicam_output", QVariant::fromValue(multicam_tex)); + } + } + + return db; +} + void RenderProcessor::Process(RenderTicketPtr ticket, Renderer *render_ctx, DecoderCache *decoder_cache, ShaderCache *shader_cache) { RenderProcessor p(ticket, render_ctx, decoder_cache, shader_cache); p.Run(); } -NodeValueTable RenderProcessor::GenerateBlockTable(const Track *track, const TimeRange &range) -{ - if (track->type() == Track::kAudio) { - - const AudioParams& audio_params = GetCacheAudioParams(); - - QVector active_blocks = track->BlocksAtTimeRange(range); - - // All these blocks will need to output to a buffer so we create one here - SampleBuffer block_range_buffer(audio_params, range.length()); - block_range_buffer.silence(); - - NodeValueTable merged_table; - - // Loop through active blocks retrieving their audio - foreach (Block* b, active_blocks) { - if (dynamic_cast(b) || dynamic_cast(b)) { - TimeRange range_for_block(qMax(b->in(), range.in()), - qMin(b->out(), range.out())); - - qint64 destination_offset = audio_params.time_to_samples(range_for_block.in() - range.in()); - qint64 max_dest_sz = audio_params.time_to_samples(range_for_block.length()); - - // Destination buffer - NodeValueTable table = GenerateTable(b, Track::TransformRangeForBlock(b, range_for_block)); - SampleBuffer samples_from_this_block = table.Take(NodeValue::kSamples).toSamples(); - ClipBlock *clip_cast = dynamic_cast(b); - - if (samples_from_this_block.is_allocated()) { - // If this is a clip, we might have extra speed/reverse information - if (clip_cast) { - double speed_value = clip_cast->speed(); - bool reversed = clip_cast->reverse(); - - if (qIsNull(speed_value)) { - // Just silence, don't think there's any other practical application of 0 speed audio - samples_from_this_block.silence(); - } else if (!qFuzzyCompare(speed_value, 1.0)) { - if (clip_cast->maintain_audio_pitch()) { - AudioProcessor processor; - - if (processor.Open(samples_from_this_block.audio_params(), samples_from_this_block.audio_params(), speed_value)) { - AudioProcessor::Buffer out; - - // FIXME: This is not the best way to do this, the TempoProcessor works best - // when it's given a continuous stream of audio, which is challenging - // in our current "modular" audio system. This should still work reasonably - // well on export (assuming audio is all generated at once on export), but - // users may hear clicks and pops in the audio during preview due to this - // approach. - int r = processor.Convert(samples_from_this_block.to_raw_ptrs().data(), samples_from_this_block.sample_count(), nullptr); - - if (r < 0) { - qCritical() << "Failed to change tempo of audio:" << r; - } else { - processor.Flush(); - - processor.Convert(nullptr, 0, &out); - - if (!out.empty()) { - int nb_samples = out.front().size() * samples_from_this_block.audio_params().bytes_per_sample_per_channel(); - - if (nb_samples) { - SampleBuffer new_samples(samples_from_this_block.audio_params(), nb_samples); - - for (int i=0; iproperty("type").value() != RenderManager::kTypeVideo) { diff --git a/app/render/renderprocessor.h b/app/render/renderprocessor.h index 37924181f..b3dd29f59 100644 --- a/app/render/renderprocessor.h +++ b/app/render/renderprocessor.h @@ -32,6 +32,8 @@ namespace olive { class RenderProcessor : public NodeTraverser { public: + virtual NodeValueDatabase GenerateDatabase(const Node *node, const TimeRange &range) override; + static void Process(RenderTicketPtr ticket, Renderer* render_ctx, DecoderCache* decoder_cache, ShaderCache* shader_cache); struct RenderedWaveform { @@ -42,8 +44,6 @@ public: }; protected: - virtual NodeValueTable GenerateBlockTable(const Track *track, const TimeRange &range) override; - virtual void ProcessVideoFootage(TexturePtr destination, const FootageJob *stream, const rational &input_time) override; virtual void ProcessAudioFootage(SampleBuffer &destination, const FootageJob *stream, const TimeRange &input_time) override; diff --git a/app/widget/CMakeLists.txt b/app/widget/CMakeLists.txt index 5f4ab85e6..ef967b7bd 100644 --- a/app/widget/CMakeLists.txt +++ b/app/widget/CMakeLists.txt @@ -30,6 +30,7 @@ add_subdirectory(handmovableview) add_subdirectory(keyframeview) add_subdirectory(manageddisplay) add_subdirectory(menu) +add_subdirectory(multicam) add_subdirectory(nodecombobox) add_subdirectory(nodeparamview) add_subdirectory(nodetableview) diff --git a/app/widget/manageddisplay/manageddisplay.cpp b/app/widget/manageddisplay/manageddisplay.cpp index 62afcb876..a5446162a 100644 --- a/app/widget/manageddisplay/manageddisplay.cpp +++ b/app/widget/manageddisplay/manageddisplay.cpp @@ -275,6 +275,14 @@ void ManagedDisplayWidget::SetInnerMouseTracking(bool e) } } +VideoParams ManagedDisplayWidget::GetViewportParams() const +{ + int device_width = width() * devicePixelRatioF(); + int device_height = height() * devicePixelRatioF(); + VideoParams::Format device_format = static_cast(OLIVE_CONFIG("OfflinePixelFormat").toInt()); + return VideoParams(device_width, device_height, device_format, VideoParams::kInternalChannelCount); +} + void ManagedDisplayWidget::update() { if (RenderManager::instance()->backend() == RenderManager::kOpenGL) { diff --git a/app/widget/manageddisplay/manageddisplay.h b/app/widget/manageddisplay/manageddisplay.h index 644ab0711..c26008e77 100644 --- a/app/widget/manageddisplay/manageddisplay.h +++ b/app/widget/manageddisplay/manageddisplay.h @@ -58,7 +58,8 @@ protected: virtual void initializeGL() override { connect(context(), &QOpenGLContext::aboutToBeDestroyed, - this, &ManagedDisplayWidgetOpenGL::OnDestroy); + this, &ManagedDisplayWidgetOpenGL::DestroyListener, + Qt::DirectConnection); emit OnInit(); } @@ -211,6 +212,8 @@ protected: return wrapper_ ? wrapper_->rect() : QRect(); } + VideoParams GetViewportParams() const; + protected slots: /** * @brief Called whenever the internal rendering context has been created diff --git a/app/widget/menu/menushared.cpp b/app/widget/menu/menushared.cpp index 6332046ba..1ff70f954 100644 --- a/app/widget/menu/menushared.cpp +++ b/app/widget/menu/menushared.cpp @@ -293,7 +293,7 @@ void MenuShared::EnableDisableTriggered() void MenuShared::NestTriggered() { - qDebug() << "FIXME: Stub"; + PanelManager::instance()->MostRecentlyFocused()->NestSelectedClips(); } void MenuShared::DefaultTransitionTriggered() diff --git a/app/widget/multicam/CMakeLists.txt b/app/widget/multicam/CMakeLists.txt new file mode 100644 index 000000000..2e7df53a7 --- /dev/null +++ b/app/widget/multicam/CMakeLists.txt @@ -0,0 +1,24 @@ +# Olive - Non-Linear Video Editor +# Copyright (C) 2022 Olive Team +# +# 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 . + +set(OLIVE_SOURCES + ${OLIVE_SOURCES} + widget/multicam/multicamdisplay.cpp + widget/multicam/multicamdisplay.h + widget/multicam/multicamwidget.cpp + widget/multicam/multicamwidget.h + PARENT_SCOPE +) diff --git a/app/widget/multicam/multicamdisplay.cpp b/app/widget/multicam/multicamdisplay.cpp new file mode 100644 index 000000000..9fce75e68 --- /dev/null +++ b/app/widget/multicam/multicamdisplay.cpp @@ -0,0 +1,172 @@ +/*** + + Olive - Non-Linear Video Editor + Copyright (C) 2022 Olive Team + + This program is free software: you can redistribute it and/or modify + it under the terms of the GNU General Public License as published by + the Free Software Foundation, either version 3 of the License, or + (at your option) any later version. + + This program is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU General Public License for more details. + + You should have received a copy of the GNU General Public License + along with this program. If not, see . + +***/ + +#include "multicamdisplay.h" + +namespace olive { + +#define super ViewerDisplayWidget + +MulticamDisplay::MulticamDisplay(QWidget *parent) : + super(parent), + node_(nullptr), + rows_(0), + cols_(0) +{ +} + +void MulticamDisplay::OnPaint() +{ + super::OnPaint(); + + if (node_) { + QPainter p(paint_device()); + + p.setPen(QPen(Qt::yellow, fontMetrics().height()/4)); + p.setBrush(Qt::NoBrush); + + int rows, cols; + node_->GetRowsAndColumns(&rows, &cols); + + int multi = std::max(rows, cols); + int cell_width = width() / multi; + int cell_height = height() / multi; + + int col, row; + node_->IndexToRowCols(node_->GetCurrentSource(), rows, cols, &row, &col); + + QRect r(cell_width * col, cell_height * row, cell_width, cell_height); + p.drawRect(GenerateWorldTransform().mapRect(r)); + } +} + +void MulticamDisplay::OnDestroy() +{ + shader_ = QVariant(); +} + +TexturePtr MulticamDisplay::LoadCustomTextureFromFrame(const QVariant &v) +{ + if (v.canConvert >()) { + QVector tex = v.value >(); + + TexturePtr main = renderer()->CreateTexture(this->GetViewportParams()); + + int rows, cols; + MultiCamNode::GetRowsAndColumns(tex.size(), &rows, &cols); + + if (shader_.isNull() || rows_ != rows || cols_ != cols) { + if (!shader_.isNull()) { + renderer()->DestroyNativeShader(shader_); + } + + shader_ = renderer()->CreateNativeShader(ShaderCode(GenerateShaderCode(rows, cols))); + + rows_ = rows; + cols_ = cols; + } + + ShaderJob job; + + for (int i=0; iBlitToTexture(shader_, job, main.get()); + + return main; + } else { + return super::LoadCustomTextureFromFrame(v); + } +} + +QString dblToGlsl(double d) +{ + return QString::number(d, 'f'); +} + +QString MulticamDisplay::GenerateShaderCode(int rows, int cols) +{ + int multiplier = std::max(cols, rows); + + QStringList shader; + + shader.append(QStringLiteral("in vec2 ove_texcoord;")); + shader.append(QStringLiteral("out vec4 frag_color;")); + + for (int x=0;x 0) { + shader.append(QStringLiteral(" else")); + } + if (x == cols-1) { + shader.append(QStringLiteral(" {")); + } else { + shader.append(QStringLiteral(" if (ove_texcoord.x < %1) {").arg(dblToGlsl(double(x+1)/double(multiplier)))); + } + + for (int y=0;y 0) { + shader.append(QStringLiteral(" else")); + } + if (y == rows-1) { + shader.append(QStringLiteral(" {")); + } else { + shader.append(QStringLiteral(" if (ove_texcoord.y < %1) {").arg(dblToGlsl(double(y+1)/double(multiplier)))); + } + QString input = QStringLiteral("tex_%1_%2").arg(QString::number(y), QString::number(x)); + shader.append(QStringLiteral(" vec2 coord = vec2((ove_texcoord.x+%1)*%2, (ove_texcoord.y+%3)*%4);").arg( + dblToGlsl( - double(x)/double(multiplier)), + dblToGlsl(multiplier), + dblToGlsl( - double(y)/double(multiplier)), + dblToGlsl(multiplier) + )); + shader.append(QStringLiteral(" if (%1_enabled && coord.x >= 0.0 && coord.x < 1.0 && coord.y >= 0.0 && coord.y < 1.0) {").arg(input)); + shader.append(QStringLiteral(" frag_color = texture(%1, coord);").arg(input)); + shader.append(QStringLiteral(" } else {")); + shader.append(QStringLiteral(" discard;")); + shader.append(QStringLiteral(" }")); + shader.append(QStringLiteral(" }")); + } + + shader.append(QStringLiteral(" }")); + } + + shader.append(QStringLiteral("}")); + + return shader.join('\n'); +} + +void MulticamDisplay::SetMulticamNode(MultiCamNode *n) +{ + node_ = n; +} + +} diff --git a/app/widget/multicam/multicamdisplay.h b/app/widget/multicam/multicamdisplay.h new file mode 100644 index 000000000..4d926f5f1 --- /dev/null +++ b/app/widget/multicam/multicamdisplay.h @@ -0,0 +1,57 @@ +/*** + + Olive - Non-Linear Video Editor + Copyright (C) 2022 Olive Team + + 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 . + +***/ + +#ifndef MULTICAMDISPLAY_H +#define MULTICAMDISPLAY_H + +#include "node/input/multicam/multicamnode.h" +#include "widget/viewer/viewerdisplay.h" + +namespace olive { + +class MulticamDisplay : public ViewerDisplayWidget +{ + Q_OBJECT +public: + explicit MulticamDisplay(QWidget *parent = nullptr); + + void SetMulticamNode(MultiCamNode *n); + +protected: + virtual void OnPaint() override; + + virtual void OnDestroy() override; + + virtual TexturePtr LoadCustomTextureFromFrame(const QVariant &v) override; + +private: + static QString GenerateShaderCode(int rows, int cols); + + MultiCamNode *node_; + + QVariant shader_; + int rows_; + int cols_; + +}; + +} + +#endif // MULTICAMDISPLAY_H diff --git a/app/widget/multicam/multicamwidget.cpp b/app/widget/multicam/multicamwidget.cpp new file mode 100644 index 000000000..6afbd9ac8 --- /dev/null +++ b/app/widget/multicam/multicamwidget.cpp @@ -0,0 +1,177 @@ +/*** + + Olive - Non-Linear Video Editor + Copyright (C) 2022 Olive Team + + This program is free software: you can redistribute it and/or modify + it under the terms of the GNU General Public License as published by + the Free Software Foundation, either version 3 of the License, or + (at your option) any later version. + + This program is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU General Public License for more details. + + You should have received a copy of the GNU General Public License + along with this program. If not, see . + +***/ + +#include "multicamwidget.h" +#include "qshortcut.h" +#include "widget/nodeparamview/nodeparamviewundo.h" +#include "widget/timeruler/timeruler.h" +#include "widget/timelinewidget/undo/timelineundosplit.h" + +namespace olive { + +#define super TimeBasedWidget + +MulticamWidget::MulticamWidget(QWidget *parent) : + super{false, false, parent}, + node_(nullptr), + clip_(nullptr) +{ + auto layout = new QVBoxLayout(this); + + sizer_ = new ViewerSizer(this); + layout->addWidget(sizer_); + + display_ = new MulticamDisplay(this); + display_->SetShowWidgetBackground(true); + connect(display_, &ViewerDisplayWidget::DragStarted, this, &MulticamWidget::DisplayClicked); + + connect(sizer_, &ViewerSizer::RequestScale, display_, &ViewerDisplayWidget::SetMatrixZoom); + connect(sizer_, &ViewerSizer::RequestTranslate, display_, &ViewerDisplayWidget::SetMatrixTranslate); + connect(display_, &ViewerDisplayWidget::HandDragMoved, sizer_, &ViewerSizer::HandDragMove); + sizer_->SetWidget(display_); + + layout->addWidget(this->ruler()); + layout->addWidget(this->scrollbar()); + + for (int i=0; i<9; i++) { + new QShortcut(QStringLiteral("Ctrl+%1").arg(QString::number(i+1)), this, this, [this, i]{Switch(i, false);}); + new QShortcut(QString::number(i+1), this, this, [this, i]{Switch(i, true);}); + } +} + +void MulticamWidget::SetMulticamNodeInternal(ViewerOutput *viewer, MultiCamNode *n, ClipBlock *clip) +{ + ConnectViewerNode(viewer); + node_ = n; + display_->SetMulticamNode(n); + clip_ = clip; +} + +void MulticamWidget::SetMulticamNode(ViewerOutput *viewer, MultiCamNode *n, ClipBlock *clip, const rational &time) +{ + if (time.isNaN() || time == GetTime()) { + SetMulticamNodeInternal(viewer, n, clip); + play_queue_.clear(); + } else { + MulticamNodeQueue m = {time, viewer, n, clip}; + play_queue_.push_back(m); + } +} + +void MulticamWidget::ConnectNodeEvent(ViewerOutput *n) +{ + connect(n, &ViewerOutput::SizeChanged, sizer_, &ViewerSizer::SetChildSize); + connect(n, &ViewerOutput::PixelAspectChanged, sizer_, &ViewerSizer::SetPixelAspectRatio); + + VideoParams vp = n->GetVideoParams(); + sizer_->SetChildSize(vp.width(), vp.height()); + sizer_->SetPixelAspectRatio(vp.pixel_aspect_ratio()); +} + +void MulticamWidget::DisconnectNodeEvent(ViewerOutput *n) +{ + disconnect(n, &ViewerOutput::SizeChanged, sizer_, &ViewerSizer::SetChildSize); + disconnect(n, &ViewerOutput::PixelAspectChanged, sizer_, &ViewerSizer::SetPixelAspectRatio); +} + +void MulticamWidget::TimeChangedEvent(const rational &t) +{ + super::TimeChangedEvent(t); + + if (!play_queue_.empty()) { + const MulticamNodeQueue &m = play_queue_.front(); + if (m.time >= t) { + SetMulticamNodeInternal(m.viewer, m.node, m.clip); + play_queue_.pop_front(); + } + } +} + +void MulticamWidget::Switch(int source, bool split_clip) +{ + if (!node_) { + return; + } + + MultiUndoCommand *command = new MultiUndoCommand(); + + MultiCamNode *cam = node_; + ClipBlock *clip = clip_; + + BlockSplitPreservingLinksCommand *split = nullptr; + + if (clip_ && split_clip && clip_->in() < GetTime() && clip_->out() > GetTime()) { + QVector blocks; + + blocks.append(clip_); + blocks.append(clip_->block_links()); + + split = new BlockSplitPreservingLinksCommand(blocks, {GetTime()}); + split->redo_now(); + command->add_child(split); + + clip = static_cast(split->GetSplit(clip_, 0)); + + cam = clip->FindMulticam(); + } + + command->add_child(new NodeParamSetStandardValueCommand(NodeKeyframeTrackReference(NodeInput(cam, cam->kCurrentInput)), source)); + + for (Block *link : clip->block_links()) { + if (ClipBlock *clink = dynamic_cast(link)) { + if (MultiCamNode *mlink = clink->FindMulticam()) { + command->add_child(new NodeParamSetStandardValueCommand(NodeKeyframeTrackReference(NodeInput(mlink, mlink->kCurrentInput)), source)); + } + } + } + + Core::instance()->undo_stack()->push(command); + + display_->update(); +} + +void MulticamWidget::DisplayClicked(const QPoint &p) +{ + if (!node_) { + return; + } + + QPointF click = display_->ScreenToScenePoint(p); + int width = display_->GetVideoParams().width(); + int height = display_->GetVideoParams().height(); + + if (click.x() < 0 || click.y() < 0 || click.x() >= width || click.y() >= height) { + return; + } + + int rows, cols; + node_->GetRowsAndColumns(&rows, &cols); + + int multi = std::max(cols, rows); + + int c = click.x() / (width/multi); + int r = click.y() / (height/multi); + + int source = node_->RowsColsToIndex(r, c, rows, cols); + + Switch(source, true); +} + +} diff --git a/app/widget/multicam/multicamwidget.h b/app/widget/multicam/multicamwidget.h new file mode 100644 index 000000000..8691c5668 --- /dev/null +++ b/app/widget/multicam/multicamwidget.h @@ -0,0 +1,75 @@ +/*** + + Olive - Non-Linear Video Editor + Copyright (C) 2022 Olive Team + + 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 . + +***/ + +#ifndef MULTICAMWIDGET_H +#define MULTICAMWIDGET_H + +#include "multicamdisplay.h" +#include "node/input/multicam/multicamnode.h" +#include "widget/viewer/viewer.h" + +namespace olive { + +class MulticamWidget : public TimeBasedWidget +{ + Q_OBJECT +public: + explicit MulticamWidget(QWidget *parent = nullptr); + + MulticamDisplay *GetDisplayWidget() const { return display_; } + + void SetMulticamNode(ViewerOutput *viewer, MultiCamNode *n, ClipBlock *clip, const rational &time); + +protected: + virtual void ConnectNodeEvent(ViewerOutput *n) override; + virtual void DisconnectNodeEvent(ViewerOutput *n) override; + virtual void TimeChangedEvent(const rational &t) override; + +private: + void SetMulticamNodeInternal(ViewerOutput *viewer, MultiCamNode *n, ClipBlock *clip); + + void Switch(int source, bool split_clip); + + ViewerSizer *sizer_; + + MulticamDisplay *display_; + + MultiCamNode *node_; + + ClipBlock *clip_; + + struct MulticamNodeQueue + { + rational time; + ViewerOutput *viewer; + MultiCamNode *node; + ClipBlock *clip; + }; + + std::list play_queue_; + +private slots: + void DisplayClicked(const QPoint &p); + +}; + +} + +#endif // MULTICAMWIDGET_H diff --git a/app/widget/nodeparamview/nodeparamview.cpp b/app/widget/nodeparamview/nodeparamview.cpp index 5a6254bd5..b0d6f1618 100644 --- a/app/widget/nodeparamview/nodeparamview.cpp +++ b/app/widget/nodeparamview/nodeparamview.cpp @@ -76,7 +76,7 @@ NodeParamView::NodeParamView(bool create_keyframe_view, QWidget *parent) : // Create contexts for three different types context_items_.resize(Track::kCount + 1); for (int i=0; isetVisible(false); connect(c, &NodeParamViewContext::AboutToDeleteItem, this, &NodeParamView::ItemAboutToBeRemoved, Qt::DirectConnection); @@ -245,8 +245,6 @@ void NodeParamView::DeselectNodes(const QVector &nodes) void NodeParamView::UpdateContexts() { - //TIME_THIS_FUNCTION; - bool changes_made = false; foreach (Node *ctx, current_contexts_) { @@ -735,7 +733,7 @@ void NodeParamView::AddNode(Node *n, Node *ctx, NodeParamViewContext *context) return; } - NodeParamViewItem* item = new NodeParamViewItem(n, IsGroupMode() ? kCheckBoxesOnNonConnected : kNoCheckBoxes, context); + NodeParamViewItem* item = new NodeParamViewItem(n, IsGroupMode() ? kCheckBoxesOnNonConnected : kNoCheckBoxes, context->GetDockArea()); connect(item, &NodeParamViewItem::RequestSetTime, this, &NodeParamView::SetTimeAndSignal); connect(item, &NodeParamViewItem::RequestSelectNode, this, &NodeParamView::SelectNodeFromConnectedLink); diff --git a/app/widget/nodeparamview/nodeparamviewconnectedlabel.cpp b/app/widget/nodeparamview/nodeparamviewconnectedlabel.cpp index 824452f4b..937037219 100644 --- a/app/widget/nodeparamview/nodeparamviewconnectedlabel.cpp +++ b/app/widget/nodeparamview/nodeparamviewconnectedlabel.cpp @@ -50,13 +50,13 @@ NodeParamViewConnectedLabel::NodeParamViewConnectedLabel(const NodeInput &input, label_layout->setMargin(0); layout->addLayout(label_layout); - CollapseButton *collapse_btn = new CollapseButton(); + CollapseButton *collapse_btn = new CollapseButton(this); collapse_btn->setChecked(false); label_layout->addWidget(collapse_btn); - label_layout->addWidget(new QLabel(tr("Connected to"))); + label_layout->addWidget(new QLabel(tr("Connected to"), this)); - connected_to_lbl_ = new ClickableLabel(); + connected_to_lbl_ = new ClickableLabel(this); connected_to_lbl_->setCursor(Qt::PointingHandCursor); connected_to_lbl_->setContextMenuPolicy(Qt::CustomContextMenu); connect(connected_to_lbl_, &ClickableLabel::MouseClicked, this, &NodeParamViewConnectedLabel::ConnectionClicked); @@ -80,18 +80,23 @@ NodeParamViewConnectedLabel::NodeParamViewConnectedLabel(const NodeInput &input, connect(input_.node(), &Node::InputConnected, this, &NodeParamViewConnectedLabel::InputConnected); connect(input_.node(), &Node::InputDisconnected, this, &NodeParamViewConnectedLabel::InputDisconnected); - // Set up table area - value_tree_ = new NodeValueTree(); - value_tree_->setVisible(false); - layout->addWidget(value_tree_); + // Creating the tree is expensive, hold off until the user specifically requests it + value_tree_ = nullptr; connect(collapse_btn, &CollapseButton::toggled, this, &NodeParamViewConnectedLabel::SetValueTreeVisible); } +void NodeParamViewConnectedLabel::CreateTree() +{ + // Set up table area + value_tree_ = new NodeValueTree(this); + layout()->addWidget(value_tree_); +} + void NodeParamViewConnectedLabel::SetTime(const rational &time) { time_ = time; - if (value_tree_->isVisible()) { + if (value_tree_ && value_tree_->isVisible()) { UpdateValueTree(); } } @@ -154,14 +159,22 @@ void NodeParamViewConnectedLabel::UpdateLabel() void NodeParamViewConnectedLabel::UpdateValueTree() { - value_tree_->SetNode(input_, time_); + if (value_tree_) { + value_tree_->SetNode(input_, time_); + } } void NodeParamViewConnectedLabel::SetValueTreeVisible(bool e) { - value_tree_->setVisible(e); + if (value_tree_) { + value_tree_->setVisible(e); + } if (e) { + if (!value_tree_) { + CreateTree(); + } + UpdateValueTree(); } } diff --git a/app/widget/nodeparamview/nodeparamviewconnectedlabel.h b/app/widget/nodeparamview/nodeparamviewconnectedlabel.h index 183ff633b..9a7a81ee7 100644 --- a/app/widget/nodeparamview/nodeparamviewconnectedlabel.h +++ b/app/widget/nodeparamview/nodeparamviewconnectedlabel.h @@ -51,6 +51,8 @@ private: void UpdateValueTree(); + void CreateTree(); + ClickableLabel* connected_to_lbl_; NodeInput input_; diff --git a/app/widget/nodeparamview/nodeparamviewitem.cpp b/app/widget/nodeparamview/nodeparamviewitem.cpp index 7b0e82fd3..a27cbe824 100644 --- a/app/widget/nodeparamview/nodeparamviewitem.cpp +++ b/app/widget/nodeparamview/nodeparamviewitem.cpp @@ -88,7 +88,7 @@ void NodeParamViewItem::RecreateBody() body_->deleteLater(); } - body_ = new NodeParamViewItemBody(node_, create_checkboxes_); + body_ = new NodeParamViewItemBody(node_, create_checkboxes_, this); connect(body_, &NodeParamViewItemBody::RequestSelectNode, this, &NodeParamViewItem::RequestSelectNode); connect(body_, &NodeParamViewItemBody::RequestSetTime, this, &NodeParamViewItem::RequestSetTime); connect(body_, &NodeParamViewItemBody::ArrayExpandedChanged, this, &NodeParamViewItem::ArrayExpandedChanged); @@ -148,7 +148,7 @@ NodeParamViewItemBody::NodeParamViewItemBody(Node* node, NodeParamViewCheckBoxBe if (n->InputIsArray(input)) { // Insert here - QWidget* array_widget = new QWidget(); + QWidget* array_widget = new QWidget(this); QGridLayout* array_layout = new QGridLayout(array_widget); array_layout->setContentsMargins(QtUtils::QFontMetricsWidth(fontMetrics(), QStringLiteral(" ")), 0, 0, 0); @@ -160,7 +160,7 @@ NodeParamViewItemBody::NodeParamViewItemBody(Node* node, NodeParamViewCheckBoxBe int arr_sz = 0; // Add one last add button for appending to the array - NodeParamViewArrayButton* append_btn = new NodeParamViewArrayButton(NodeParamViewArrayButton::kAdd); + NodeParamViewArrayButton* append_btn = new NodeParamViewArrayButton(NodeParamViewArrayButton::kAdd, this); connect(append_btn, &NodeParamViewArrayButton::clicked, this, &NodeParamViewItemBody::ArrayAppendClicked); array_layout->addWidget(append_btn, arr_sz, kArrayInsertColumn); @@ -186,7 +186,7 @@ void NodeParamViewItemBody::CreateWidgets(QGridLayout* layout, Node *node, const // Create optional checkbox if requested if (create_checkboxes_) { - ui_objects.optional_checkbox = new QCheckBox(); + ui_objects.optional_checkbox = new QCheckBox(this); connect(ui_objects.optional_checkbox, &QCheckBox::clicked, this, &NodeParamViewItemBody::OptionalCheckBoxClicked); layout->addWidget(ui_objects.optional_checkbox, row, kOptionalCheckBox); @@ -196,7 +196,7 @@ void NodeParamViewItemBody::CreateWidgets(QGridLayout* layout, Node *node, const } // Add descriptor label - ui_objects.main_label = new QLabel(); + ui_objects.main_label = new QLabel(this); // Create input label layout->addWidget(ui_objects.main_label, row, kLabelColumn); @@ -205,7 +205,7 @@ void NodeParamViewItemBody::CreateWidgets(QGridLayout* layout, Node *node, const if (element == -1) { // Create a collapse toggle for expanding/collapsing the array - CollapseButton* array_collapse_btn = new CollapseButton(); + CollapseButton* array_collapse_btn = new CollapseButton(this); // Default to collapsed array_collapse_btn->setChecked(false); @@ -220,8 +220,8 @@ void NodeParamViewItemBody::CreateWidgets(QGridLayout* layout, Node *node, const } else { - NodeParamViewArrayButton* insert_element_btn = new NodeParamViewArrayButton(NodeParamViewArrayButton::kAdd); - NodeParamViewArrayButton* remove_element_btn = new NodeParamViewArrayButton(NodeParamViewArrayButton::kRemove); + NodeParamViewArrayButton* insert_element_btn = new NodeParamViewArrayButton(NodeParamViewArrayButton::kAdd, this); + NodeParamViewArrayButton* remove_element_btn = new NodeParamViewArrayButton(NodeParamViewArrayButton::kRemove, this); layout->addWidget(insert_element_btn, row, kArrayInsertColumn); layout->addWidget(remove_element_btn, row, kArrayRemoveColumn); @@ -249,14 +249,14 @@ void NodeParamViewItemBody::CreateWidgets(QGridLayout* layout, Node *node, const if (node->IsInputConnectable(input)) { // Create clickable label used when an input is connected - ui_objects.connected_label = new NodeParamViewConnectedLabel(resolved); + ui_objects.connected_label = new NodeParamViewConnectedLabel(resolved, this); connect(ui_objects.connected_label, &NodeParamViewConnectedLabel::RequestSelectNode, this, &NodeParamViewItemBody::RequestSelectNode); layout->addWidget(ui_objects.connected_label, row, kWidgetStartColumn, 1, kKeyControlColumn - kWidgetStartColumn); } // Add keyframe control to this layout if parameter is keyframable if (node->IsInputKeyframable(input)) { - ui_objects.key_control = new NodeParamViewKeyframeControl(); + ui_objects.key_control = new NodeParamViewKeyframeControl(this); ui_objects.key_control->SetInput(resolved); layout->addWidget(ui_objects.key_control, row, kKeyControlColumn); connect(ui_objects.key_control, &NodeParamViewKeyframeControl::RequestSetTime, this, &NodeParamViewItemBody::RequestSetTime); diff --git a/app/widget/nodeparamview/nodeparamviewitemtitlebar.cpp b/app/widget/nodeparamview/nodeparamviewitemtitlebar.cpp index 0e9aa6b32..ea51f647b 100644 --- a/app/widget/nodeparamview/nodeparamviewitemtitlebar.cpp +++ b/app/widget/nodeparamview/nodeparamviewitemtitlebar.cpp @@ -33,31 +33,31 @@ NodeParamViewItemTitleBar::NodeParamViewItemTitleBar(QWidget *parent) : { QHBoxLayout* layout = new QHBoxLayout(this); - collapse_btn_ = new CollapseButton(); + collapse_btn_ = new CollapseButton(this); connect(collapse_btn_, &QPushButton::clicked, this, &NodeParamViewItemTitleBar::ExpandedStateChanged); layout->addWidget(collapse_btn_); - lbl_ = new QLabel(); + lbl_ = new QLabel(this); layout->addWidget(lbl_); // Place next buttons on the far side layout->addStretch(); - add_fx_btn_ = new QPushButton(); + add_fx_btn_ = new QPushButton(this); add_fx_btn_->setIcon(icon::AddEffect); add_fx_btn_->setFixedSize(add_fx_btn_->sizeHint().height(), add_fx_btn_->sizeHint().height()); add_fx_btn_->setVisible(false); layout->addWidget(add_fx_btn_); connect(add_fx_btn_, &QPushButton::clicked, this, &NodeParamViewItemTitleBar::AddEffectButtonClicked); - pin_btn_ = new QPushButton(QStringLiteral("P")); + pin_btn_ = new QPushButton(QStringLiteral("P"), this); pin_btn_->setCheckable(true); pin_btn_->setFixedSize(pin_btn_->sizeHint().height(), pin_btn_->sizeHint().height()); pin_btn_->setVisible(false); layout->addWidget(pin_btn_); connect(pin_btn_, &QPushButton::clicked, this, &NodeParamViewItemTitleBar::PinToggled); - enabled_checkbox_ = new QCheckBox(); + enabled_checkbox_ = new QCheckBox(this); enabled_checkbox_->setVisible(false); layout->addWidget(enabled_checkbox_); connect(enabled_checkbox_, &QCheckBox::clicked, this, &NodeParamViewItemTitleBar::EnabledCheckBoxClicked); diff --git a/app/widget/nodeparamview/nodeparamviewkeyframecontrol.h b/app/widget/nodeparamview/nodeparamviewkeyframecontrol.h index c11b206e0..0f8ffab7d 100644 --- a/app/widget/nodeparamview/nodeparamviewkeyframecontrol.h +++ b/app/widget/nodeparamview/nodeparamviewkeyframecontrol.h @@ -33,7 +33,11 @@ class NodeParamViewKeyframeControl : public QWidget, public TimeTargetObject { Q_OBJECT public: - NodeParamViewKeyframeControl(bool right_align = true, QWidget* parent = nullptr); + NodeParamViewKeyframeControl(bool right_align, QWidget* parent = nullptr); + NodeParamViewKeyframeControl(QWidget* parent = nullptr) : + NodeParamViewKeyframeControl(true, parent) + { + } const NodeInput& GetConnectedInput() const { diff --git a/app/widget/nodeparamview/nodeparamviewwidgetbridge.cpp b/app/widget/nodeparamview/nodeparamviewwidgetbridge.cpp index 9f0ae6ac8..dd33a7659 100644 --- a/app/widget/nodeparamview/nodeparamviewwidgetbridge.cpp +++ b/app/widget/nodeparamview/nodeparamviewwidgetbridge.cpp @@ -71,9 +71,11 @@ int GetSliderCount(NodeValue::Type type) void NodeParamViewWidgetBridge::CreateWidgets() { + QWidget *parent = dynamic_cast(this->parent()); + if (GetInnerInput().IsArray() && GetInnerInput().element() == -1) { - NodeParamViewArrayWidget* w = new NodeParamViewArrayWidget(GetInnerInput().node(), GetInnerInput().input()); + NodeParamViewArrayWidget* w = new NodeParamViewArrayWidget(GetInnerInput().node(), GetInnerInput().input(), parent); connect(w, &NodeParamViewArrayWidget::DoubleClicked, this, &NodeParamViewWidgetBridge::ArrayWidgetDoubleClicked); widgets_.append(w); @@ -94,12 +96,12 @@ void NodeParamViewWidgetBridge::CreateWidgets() break; case NodeValue::kInt: { - CreateSliders(1); + CreateSliders(1, parent); break; } case NodeValue::kRational: { - CreateSliders(1); + CreateSliders(1, parent); break; } case NodeValue::kFloat: @@ -107,12 +109,12 @@ void NodeParamViewWidgetBridge::CreateWidgets() case NodeValue::kVec3: case NodeValue::kVec4: { - CreateSliders(GetSliderCount(t)); + CreateSliders(GetSliderCount(t), parent); break; } case NodeValue::kCombo: { - QComboBox* combobox = new QComboBox(); + QComboBox* combobox = new QComboBox(parent); QStringList items = GetInnerInput().GetComboBoxStrings(); foreach (const QString& s, items) { @@ -125,21 +127,21 @@ void NodeParamViewWidgetBridge::CreateWidgets() } case NodeValue::kFile: { - FileField* file_field = new FileField(); + FileField* file_field = new FileField(parent); widgets_.append(file_field); connect(file_field, &FileField::FilenameChanged, this, &NodeParamViewWidgetBridge::WidgetCallback); break; } case NodeValue::kColor: { - ColorButton* color_button = new ColorButton(GetInnerInput().node()->project()->color_manager()); + ColorButton* color_button = new ColorButton(GetInnerInput().node()->project()->color_manager(), parent); widgets_.append(color_button); connect(color_button, &ColorButton::ColorChanged, this, &NodeParamViewWidgetBridge::WidgetCallback); break; } case NodeValue::kText: { - NodeParamViewTextEdit* line_edit = new NodeParamViewTextEdit(); + NodeParamViewTextEdit* line_edit = new NodeParamViewTextEdit(parent); widgets_.append(line_edit); connect(line_edit, &NodeParamViewTextEdit::textEdited, this, &NodeParamViewWidgetBridge::WidgetCallback); connect(line_edit, &NodeParamViewTextEdit::RequestEditInViewer, this, &NodeParamViewWidgetBridge::RequestEditTextInViewer); @@ -147,21 +149,21 @@ void NodeParamViewWidgetBridge::CreateWidgets() } case NodeValue::kBoolean: { - QCheckBox* check_box = new QCheckBox(); + QCheckBox* check_box = new QCheckBox(parent); widgets_.append(check_box); connect(check_box, &QCheckBox::clicked, this, &NodeParamViewWidgetBridge::WidgetCallback); break; } case NodeValue::kFont: { - QFontComboBox* font_combobox = new QFontComboBox(); + QFontComboBox* font_combobox = new QFontComboBox(parent); widgets_.append(font_combobox); connect(font_combobox, &QFontComboBox::currentFontChanged, this, &NodeParamViewWidgetBridge::WidgetCallback); break; } case NodeValue::kBezier: { - BezierWidget *bezier = new BezierWidget(); + BezierWidget *bezier = new BezierWidget(parent); widgets_.append(bezier); connect(bezier->x_slider(), &FloatSlider::ValueChanged, this, &NodeParamViewWidgetBridge::WidgetCallback); @@ -384,10 +386,10 @@ void NodeParamViewWidgetBridge::WidgetCallback() } template -void NodeParamViewWidgetBridge::CreateSliders(int count) +void NodeParamViewWidgetBridge::CreateSliders(int count, QWidget *parent) { for (int i=0;iSliderBase::SetDefaultValue(GetInnerInput().GetSplitDefaultValueForTrack(i)); fs->SetLadderElementCount(2); diff --git a/app/widget/nodeparamview/nodeparamviewwidgetbridge.h b/app/widget/nodeparamview/nodeparamviewwidgetbridge.h index 98a71d5d7..772febbde 100644 --- a/app/widget/nodeparamview/nodeparamviewwidgetbridge.h +++ b/app/widget/nodeparamview/nodeparamviewwidgetbridge.h @@ -75,7 +75,7 @@ private: void SetProperty(const QString &key, const QVariant &value); template - void CreateSliders(int count); + void CreateSliders(int count, QWidget *parent); void UpdateWidgetValues(); diff --git a/app/widget/scope/histogram/histogram.cpp b/app/widget/scope/histogram/histogram.cpp index a0ba2eaab..da4136a5f 100644 --- a/app/widget/scope/histogram/histogram.cpp +++ b/app/widget/scope/histogram/histogram.cpp @@ -47,10 +47,10 @@ void HistogramScope::OnInit() void HistogramScope::OnDestroy() { - super::OnDestroy(); - pipeline_secondary_.clear(); texture_row_sums_ = nullptr; + + super::OnDestroy(); } ShaderCode HistogramScope::GenerateShaderCode() diff --git a/app/widget/scope/scopebase/scopebase.cpp b/app/widget/scope/scopebase/scopebase.cpp index bedcd986d..d758f0b42 100644 --- a/app/widget/scope/scopebase/scopebase.cpp +++ b/app/widget/scope/scopebase/scopebase.cpp @@ -52,9 +52,7 @@ void ScopeBase::DrawScope(TexturePtr managed_tex, QVariant pipeline) job.Insert(QStringLiteral("ove_maintex"), NodeValue(NodeValue::kTexture, QVariant::fromValue(managed_tex))); - renderer()->Blit(pipeline, job, VideoParams(width(), height(), - static_cast(OLIVE_CONFIG("OfflinePixelFormat").toInt()), - VideoParams::kInternalChannelCount)); + renderer()->Blit(pipeline, job, GetViewportParams()); } void ScopeBase::OnInit() @@ -89,11 +87,11 @@ void ScopeBase::OnPaint() void ScopeBase::OnDestroy() { - super::OnDestroy(); - managed_tex_ = nullptr; texture_ = nullptr; pipeline_.clear(); + + super::OnDestroy(); } } diff --git a/app/widget/scope/waveform/waveform.cpp b/app/widget/scope/waveform/waveform.cpp index bb6902a64..2f8f46407 100644 --- a/app/widget/scope/waveform/waveform.cpp +++ b/app/widget/scope/waveform/waveform.cpp @@ -72,9 +72,7 @@ void WaveformScope::DrawScope(TexturePtr managed_tex, QVariant pipeline) job.Insert(QStringLiteral("ove_maintex"), NodeValue(NodeValue::kTexture, QVariant::fromValue(managed_tex))); - renderer()->Blit(pipeline, job, VideoParams(width(), height(), - static_cast(OLIVE_CONFIG("OfflinePixelFormat").toInt()), - VideoParams::kInternalChannelCount)); + renderer()->Blit(pipeline, job, GetViewportParams()); float waveform_dim_x = ceil((width() - 1.0) * waveform_scale); float waveform_dim_y = ceil((height() - 1.0) * waveform_scale); diff --git a/app/widget/timelinewidget/timelinewidget.cpp b/app/widget/timelinewidget/timelinewidget.cpp index f54792aef..9941d061a 100644 --- a/app/widget/timelinewidget/timelinewidget.cpp +++ b/app/widget/timelinewidget/timelinewidget.cpp @@ -539,12 +539,16 @@ void TimelineWidget::DecreaseTrackHeight() void TimelineWidget::InsertFootageAtPlayhead(const QVector& footage) { - import_tool_->PlaceAt(footage, GetTime(), true); + auto command = new MultiUndoCommand(); + import_tool_->PlaceAt(footage, GetTime(), true, command); + Core::instance()->undo_stack()->push(command); } void TimelineWidget::OverwriteFootageAtPlayhead(const QVector &footage) { - import_tool_->PlaceAt(footage, GetTime(), false); + auto command = new MultiUndoCommand(); + import_tool_->PlaceAt(footage, GetTime(), false, command); + Core::instance()->undo_stack()->push(command); } void TimelineWidget::ToggleLinksOnSelected() @@ -788,13 +792,14 @@ void TimelineWidget::RecordingCallback(const QString &filename, const TimeRange task.Start(); MultiUndoCommand *import_command = task.GetCommand(); - Core::instance()->undo_stack()->pushIfHasChildren(import_command); if (task.GetImportedFootage().empty()) { qCritical() << "Failed to import recorded audio file" << filename; } else { - import_tool_->PlaceAt({task.GetImportedFootage().front()}, time.in(), false, track.index()); + import_tool_->PlaceAt({task.GetImportedFootage().front()}, time.in(), false, import_command, track.index()); } + + Core::instance()->undo_stack()->pushIfHasChildren(import_command); } void TimelineWidget::EnableRecordingOverlay(const TimelineCoordinate &coord) @@ -839,6 +844,95 @@ void TimelineWidget::AddTentativeSubtitleTrack() } } +void TimelineWidget::NestSelectedClips() +{ + if (!GetConnectedNode()) { + return; + } + + QVector blocks = this->selected_blocks_; + if (blocks.empty()) { + return; + } + + QVector tracks(blocks.size()); + QVector times(blocks.size()); + QVector track_offset(Track::kCount, INT_MAX); + rational start_time = RATIONAL_MAX; + rational end_time = RATIONAL_MIN; + for (int i=0; itrack()->ToReference();; + tracks[i] = tf; + times[i] = b->range(); + + int &to = track_offset[tf.type()]; + to = std::min(to, tf.index()); + + start_time = std::min(start_time, b->in()); + end_time = std::max(end_time, b->out()); + } + + auto move_to_nest_command = new MultiUndoCommand(); + + // Remove blocks from this sequence + ReplaceBlocksWithGaps(blocks, false, move_to_nest_command); + + // Create new sequence + Project *project = this->GetConnectedNode()->project(); + Sequence *nest = Core::CreateNewSequenceForProject(tr("Nested Sequence %1"), project); + nest->SetVideoParams(GetConnectedNode()->GetVideoParams()); + nest->SetAudioParams(GetConnectedNode()->GetAudioParams()); + move_to_nest_command->add_child(new NodeAddCommand(project, nest)); + + // Add to same folder + move_to_nest_command->add_child(new FolderAddChild(this->GetConnectedNode()->folder(), nest)); + + // Place blocks in new sequence + for (int i=0; iadd_child(new TrackPlaceBlockCommand(nest->track_list(track.type()), + track.index() - track_offset.at(track.type()), + b, range.in() - start_time)); + } + + // Do this command now, because we later do checks and actions that rely on these having been done + move_to_nest_command->redo_now(); + + auto meta_command = new MultiUndoCommand(); + meta_command->add_child(move_to_nest_command); + + // Find first free track index + bool empty = false; + int index = -1; + while (!empty) { + index++; + empty = true; + for (int i=0; itrack_list(static_cast(i)); + if (index < list->GetTrackCount() && !list->GetTrackAt(index)->IsRangeFree(TimeRange(start_time, end_time))) { + empty = false; + break; + } + } + } + + // Place new sequence in this sequence + import_tool_->PlaceAt({nest}, start_time, false, meta_command, index); + + Core::instance()->undo_stack()->push(meta_command); +} + void TimelineWidget::ClearTentativeSubtitleTrack() { if (subtitle_show_command_) { @@ -1140,6 +1234,31 @@ void TimelineWidget::ShowContextMenu() QAction *reveal_in_project = menu.addAction(tr("Reveal in Project")); reveal_in_project->setData(reinterpret_cast(clip->connected_viewer())); connect(reveal_in_project, &QAction::triggered, this, &TimelineWidget::RevealInProject); + + /*if (Sequence *sequence = dynamic_cast(clip->connected_viewer())) { + Menu *multicam_menu = new Menu(tr("Multi-Cam"), &menu); + menu.addMenu(multicam_menu); + + QAction *multicam_enabled = multicam_menu->addAction(tr("Enabled")); + multicam_enabled->setCheckable(true); + + auto mcn = sequence->FindOutputNode(); + multicam_enabled->setChecked(!mcn.empty()); + + multicam_menu->addSeparator(); + + QAction *multicam_update = multicam_menu->addAction(tr("Update")); + multicam_update->setEnabled(!mcn.empty()); + + if (!mcn.empty()) { + auto n = mcn.first(); + multicam_enabled->setProperty("multicam", Node::PtrToValue(n)); + multicam_update->setProperty("multicam", Node::PtrToValue(n)); + } + + connect(multicam_enabled, &QAction::triggered, this, &TimelineWidget::MulticamEnabledTriggered); + connect(multicam_update, &QAction::triggered, this, &TimelineWidget::MulticamUpdateTriggered); + }*/ } } @@ -1371,6 +1490,20 @@ void TimelineWidget::CacheDiscard() } } +void TimelineWidget::MulticamEnabledTriggered(bool e) +{ + if (e) { + // Add multicam node + } else if (MultiCamNode *m = Node::ValueToPtr(sender()->property("multicam"))) { + // Remove multicam node + } +} + +void TimelineWidget::MulticamUpdateTriggered() +{ + // Update multicam node +} + void TimelineWidget::AddGhost(TimelineViewGhostItem *ghost) { ghost_items_.append(ghost); diff --git a/app/widget/timelinewidget/timelinewidget.h b/app/widget/timelinewidget/timelinewidget.h index d952c5419..998e56869 100644 --- a/app/widget/timelinewidget/timelinewidget.h +++ b/app/widget/timelinewidget/timelinewidget.h @@ -112,6 +112,8 @@ public: void AddTentativeSubtitleTrack(); + void NestSelectedClips(); + /** * @brief Timelines should always be connected to sequences */ @@ -444,6 +446,9 @@ private slots: void CacheClipsInOut(); void CacheDiscard(); + void MulticamEnabledTriggered(bool e); + void MulticamUpdateTriggered(); + }; } diff --git a/app/widget/timelinewidget/tool/import.cpp b/app/widget/timelinewidget/tool/import.cpp index 02dec714d..caa0037ed 100644 --- a/app/widget/timelinewidget/tool/import.cpp +++ b/app/widget/timelinewidget/tool/import.cpp @@ -183,7 +183,9 @@ void ImportTool::DragLeave(QDragLeaveEvent* event) void ImportTool::DragDrop(TimelineViewMouseEvent *event) { if (!dragged_footage_.isEmpty()) { - DropGhosts(event->GetModifiers() & Qt::ControlModifier); + auto command = new MultiUndoCommand(); + DropGhosts(event->GetModifiers() & Qt::ControlModifier, command); + Core::instance()->undo_stack()->pushIfHasChildren(command); event->accept(); } else { @@ -191,7 +193,7 @@ void ImportTool::DragDrop(TimelineViewMouseEvent *event) } } -void ImportTool::PlaceAt(const QVector &footage, const rational &start, bool insert, int track_offset) +void ImportTool::PlaceAt(const QVector &footage, const rational &start, bool insert, MultiUndoCommand *command, int track_offset) { DraggedFootageData refs; @@ -199,10 +201,10 @@ void ImportTool::PlaceAt(const QVector &footage, const rational refs.append({f, f->GetEnabledStreamsAsReferences()}); } - PlaceAt(refs, start, insert, track_offset); + PlaceAt(refs, start, insert, command, track_offset); } -void ImportTool::PlaceAt(const DraggedFootageData &footage, const rational &start, bool insert, int track_offset) +void ImportTool::PlaceAt(const DraggedFootageData &footage, const rational &start, bool insert, MultiUndoCommand *command, int track_offset) { dragged_footage_ = footage; @@ -211,7 +213,7 @@ void ImportTool::PlaceAt(const DraggedFootageData &footage, const rational &star } PrepGhosts(start, track_offset); - DropGhosts(insert); + DropGhosts(insert, command); } void ImportTool::FootageToGhosts(rational ghost_start, const DraggedFootageData &sorted, const rational& dest_tb, const int& track_start) @@ -292,9 +294,9 @@ void ImportTool::PrepGhosts(const rational& frame, const int& track_index) } } -void ImportTool::DropGhosts(bool insert) +void ImportTool::DropGhosts(bool insert, MultiUndoCommand *parent_command) { - MultiUndoCommand* command = new MultiUndoCommand(); + auto command = new MultiUndoCommand(); if (MultiUndoCommand *c = parent()->TakeSubtitleSectionCommand()) { command->add_child(c); @@ -500,7 +502,10 @@ void ImportTool::DropGhosts(bool insert) command->add_child(new OpenSequenceCommand(sequence)); } - Core::instance()->undo_stack()->pushIfHasChildren(command); + // Do command now because RequestInvalidatedFromConnected relies on track type, which will be + // "none" before this command is done because it won't be connected to any track + command->redo_now(); + parent_command->add_child(command); while (!imported_clips.empty()) { imported_clips.front()->RequestInvalidatedFromConnected(); diff --git a/app/widget/timelinewidget/tool/import.h b/app/widget/timelinewidget/tool/import.h index 565d8f0ce..765fd9d04 100644 --- a/app/widget/timelinewidget/tool/import.h +++ b/app/widget/timelinewidget/tool/import.h @@ -37,8 +37,8 @@ public: using DraggedFootageData = QVector > >; - void PlaceAt(const QVector &footage, const rational& start, bool insert, int track_offset = 0); - void PlaceAt(const DraggedFootageData &footage, const rational& start, bool insert, int track_offset = 0); + void PlaceAt(const QVector &footage, const rational& start, bool insert, MultiUndoCommand *command, int track_offset = 0); + void PlaceAt(const DraggedFootageData &footage, const rational& start, bool insert, MultiUndoCommand *command, int track_offset = 0); enum DropWithoutSequenceBehavior { kDWSAsk, @@ -52,7 +52,7 @@ private: void PrepGhosts(const rational &frame, const int &track_index); - void DropGhosts(bool insert); + void DropGhosts(bool insert, MultiUndoCommand *parent_command); TimelineViewGhostItem* CreateGhost(const TimeRange &range, const rational &media_in, const Track::Reference &track); diff --git a/app/widget/timelinewidget/tool/transition.cpp b/app/widget/timelinewidget/tool/transition.cpp index f03f06297..3146142a8 100644 --- a/app/widget/timelinewidget/tool/transition.cpp +++ b/app/widget/timelinewidget/tool/transition.cpp @@ -178,7 +178,7 @@ bool TransitionTool::GetBlocksAtCoord(const TimelineCoordinate &coord, ClipBlock return false; } - Block* block_at_time = t->BlockAtTime(coord.GetFrame()); + Block* block_at_time = t->NearestBlockBeforeOrAt(coord.GetFrame()); if (!dynamic_cast(block_at_time)) { return false; } diff --git a/app/widget/timelinewidget/undo/timelineundosplit.cpp b/app/widget/timelinewidget/undo/timelineundosplit.cpp index da810f321..8386f6387 100644 --- a/app/widget/timelinewidget/undo/timelineundosplit.cpp +++ b/app/widget/timelinewidget/undo/timelineundosplit.cpp @@ -97,62 +97,68 @@ void BlockSplitCommand::undo() // // BlockSplitPreservingLinksCommand // -void BlockSplitPreservingLinksCommand::redo() +Block *BlockSplitPreservingLinksCommand::GetSplit(Block *original, int time_index) const { - if (commands_.isEmpty()) { - QVector< QVector > split_blocks(times_.size()); - - for (int i=0;i times_.at(i-1)); - - QVector splits(blocks_.size()); - - for (int j=0;jin() < time && b->out() > time) { - BlockSplitCommand* split_command = new BlockSplitCommand(b, time); - split_command->redo_now(); - splits.replace(j, split_command->new_block()); - commands_.append(split_command); - } else { - splits.replace(j, nullptr); - } - } - - split_blocks.replace(i, splits); + if (time_index >= 0 && time_index < times_.size()) { + int original_index = blocks_.indexOf(original); + if (original_index != -1) { + return splits_.at(time_index).at(original_index); } + } - // Now that we've determined all the splits, we can relink everything - for (int i=0;i times_.at(i-1)); - foreach (const QVector& split_list, split_blocks) { - NodeLinkCommand* blc = new NodeLinkCommand(split_list.at(i), split_list.at(j), true); - blc->redo_now(); - commands_.append(blc); - } - } + QVector splits(blocks_.size()); + + for (int j=0;jin() < time && b->out() > time) { + BlockSplitCommand* split_command = new BlockSplitCommand(b, time); + split_command->redo_now(); + splits.replace(j, split_command->new_block()); + commands_.append(split_command); + } else { + splits.replace(j, nullptr); } } - } else { - for (int i=0; iredo_now(); + + splits_.replace(i, splits); + } + + // Now that we've determined all the splits, we can relink everything + for (int i=0;i& split_list, splits_) { + NodeLinkCommand* blc = new NodeLinkCommand(split_list.at(i), split_list.at(j), true); + blc->redo_now(); + commands_.append(blc); + } + } } } } diff --git a/app/widget/timelinewidget/undo/timelineundosplit.h b/app/widget/timelinewidget/undo/timelineundosplit.h index 1fe9f9122..f7014d0e9 100644 --- a/app/widget/timelinewidget/undo/timelineundosplit.h +++ b/app/widget/timelinewidget/undo/timelineundosplit.h @@ -91,8 +91,17 @@ public: return blocks_.first()->project(); } + Block *GetSplit(Block *original, int time_index) const; + protected: - virtual void redo() override; + virtual void prepare() override; + + virtual void redo() override + { + for (int i=0; iredo_now(); + } + } virtual void undo() override { @@ -108,6 +117,8 @@ private: QVector commands_; + QVector< QVector > splits_; + }; class TrackSplitAtTimeCommand : public UndoCommand { diff --git a/app/widget/viewer/audiowaveformview.cpp b/app/widget/viewer/audiowaveformview.cpp index 9b5403942..10a4c85eb 100644 --- a/app/widget/viewer/audiowaveformview.cpp +++ b/app/widget/viewer/audiowaveformview.cpp @@ -53,7 +53,7 @@ void AudioWaveformView::SetViewer(ViewerOutput *playback) pool_.clear(); pool_.waitForDone(); - disconnect(playback_, &ViewerOutput::ConnectedWaveformChanged, this, static_cast(&AudioWaveformView::update)); + disconnect(playback_, &ViewerOutput::ConnectedWaveformChanged, viewport(), static_cast(&QWidget::update)); SetTimebase(0); } @@ -61,7 +61,7 @@ void AudioWaveformView::SetViewer(ViewerOutput *playback) playback_ = playback; if (playback_) { - connect(playback_, &ViewerOutput::ConnectedWaveformChanged, this, static_cast(&AudioWaveformView::update)); + connect(playback_, &ViewerOutput::ConnectedWaveformChanged, viewport(), static_cast(&QWidget::update)); SetTimebase(playback_->GetAudioParams().sample_rate_as_time_base()); } diff --git a/app/widget/viewer/viewer.cpp b/app/widget/viewer/viewer.cpp index 134874425..1ec330c19 100644 --- a/app/widget/viewer/viewer.cpp +++ b/app/widget/viewer/viewer.cpp @@ -40,10 +40,13 @@ #include "node/block/gap/gap.h" #include "node/generator/shape/shapenodebase.h" #include "node/project/project.h" +#include "panel/multicam/multicampanel.h" +#include "panel/panelmanager.h" #include "render/rendermanager.h" #include "viewerpreventsleep.h" #include "widget/audiomonitor/audiomonitor.h" #include "widget/menu/menu.h" +#include "widget/multicam/multicamdisplay.h" #include "widget/nodeparamview/nodeparamviewundo.h" #include "widget/timelinewidget/tool/add.h" #include "widget/timeruler/timeruler.h" @@ -62,7 +65,7 @@ const rational ViewerWidget::kAudioPlaybackInterval = rational(1, 4); const rational kVideoPlaybackInterval = rational(1, 2); -ViewerWidget::ViewerWidget(QWidget *parent) : +ViewerWidget::ViewerWidget(ViewerDisplayWidget *display, QWidget *parent) : super(false, true, parent), playback_speed_(0), color_menu_enabled_(true), @@ -74,7 +77,8 @@ ViewerWidget::ViewerWidget(QWidget *parent) : first_requeue_watcher_(nullptr), enable_audio_scrubbing_(true), waveform_mode_(kWFAutomatic), - ignore_scrub_(0) + ignore_scrub_(0), + multicam_panel_(nullptr) { // Set up main layout QVBoxLayout* layout = new QVBoxLayout(this); @@ -82,10 +86,9 @@ ViewerWidget::ViewerWidget(QWidget *parent) : // Create main OpenGL-based view and sizer sizer_ = new ViewerSizer(); - sizer_->setSizePolicy(QSizePolicy::Expanding, QSizePolicy::Expanding); layout->addWidget(sizer_); - display_widget_ = new ViewerDisplayWidget(); + display_widget_ = display; display_widget_->SetShowWidgetBackground(true); playback_devices_.append(display_widget_); connect(display_widget_, &ViewerDisplayWidget::customContextMenuRequested, this, &ViewerWidget::ShowContextMenu); @@ -257,10 +260,16 @@ void ViewerWidget::DisconnectNodeEvent(ViewerOutput *n) disconnect(n->video_frame_cache(), &FrameHashCache::Invalidated, this, &ViewerWidget::ViewerInvalidatedVideoRange); disconnect(n, &ViewerOutput::TextureInputChanged, this, &ViewerWidget::UpdateWaveformViewFromMode); + timeline_selected_blocks_.clear(); + node_view_selected_.clear(); + if (multicam_panel_) { + multicam_panel_->SetMulticamNode(nullptr, nullptr, nullptr, rational::NaN); + } + CloseAudioProcessor(); audio_scrub_watchers_.clear(); - SetDisplayImage(QVariant()); + SetDisplayImage(nullptr); ruler()->SetPlaybackCache(nullptr); @@ -416,6 +425,11 @@ void ViewerWidget::StartCapture(TimelineWidget *source, const TimeRange &time, c recording_track_ = track; } +void ViewerWidget::ConnectMulticamWidget(MulticamWidget *p) +{ + multicam_panel_ = p; +} + FramePtr ViewerWidget::DecodeCachedImage(const QString &cache_path, const QUuid &cache_id, const int64_t& time) { FramePtr frame = FrameHashCache::LoadCacheFrame(cache_path, cache_id, time); @@ -580,7 +594,7 @@ void ViewerWidget::RequestNextDryRun() } else { RenderTicketWatcher *watcher = new RenderTicketWatcher(this); connect(watcher, &RenderTicketWatcher::Finished, this, &ViewerWidget::DryRunFinished); - watcher->SetTicket(auto_cacher_->GetSingleFrame(next_time, true)); + watcher->SetTicket(GetSingleFrame(next_time, true)); dry_run_next_frame_ += playback_speed_; dry_run_watchers_.append(watcher); } @@ -604,6 +618,75 @@ void ViewerWidget::SetWaveformMode(WaveformMode wf) UpdateWaveformViewFromMode(); } +void ViewerWidget::DetectMulticamNode(const rational &time) +{ + // Look for multicam node + MultiCamNode *multicam = nullptr; + ClipBlock *clip = nullptr; + + // Faster way to do this + if (multicam_panel_ && multicam_panel_->isVisible()) { + if (Sequence *s = dynamic_cast(GetConnectedNode())) { + // Prefer selected nodes + for (Node *n : qAsConst(node_view_selected_)) { + if ((multicam = dynamic_cast(n))) { + // Found multicam, now try to find corresponding clip from selected timeline blocks + for (Block *b : qAsConst(timeline_selected_blocks_)) { + if (ClipBlock *c = dynamic_cast(b)) { + if (c->range().Contains(time) && c->ContextContainsNode(multicam)) { + clip = c; + break; + } + } + } + break; + } + } + + // Next, prefer multicam from selected block + if (!multicam) { + for (Block *b : qAsConst(timeline_selected_blocks_)) { + if (b->range().Contains(time)) { + if ((clip = dynamic_cast(b))) { + if ((multicam = clip->FindMulticam())) { + break; + } + } + } + } + } + + if (!multicam) { + const QVector &tracks = s->GetTracks(); + for (Track *t : tracks) { + if (t->IsLocked()) { + continue; + } + + Block *b = t->NearestBlockBeforeOrAt(time); + if ((clip = dynamic_cast(b))) { + if ((multicam = clip->FindMulticam())) { + break; + } + } + } + } + } + } + + if (multicam) { + if (multicam_panel_) { + multicam_panel_->SetMulticamNode(GetConnectedNode(), multicam, clip, time); + } + auto_cacher()->SetMulticamNode(multicam); + } else { + auto_cacher()->SetMulticamNode(nullptr); + if (multicam_panel_) { + multicam_panel_->SetMulticamNode(nullptr, nullptr, nullptr, time); + } + } +} + void ViewerWidget::UpdateWaveformViewFromMode() { bool prefer_waveform = ShouldForceWaveform(); @@ -790,6 +873,8 @@ void ViewerWidget::UpdateTextureFromNode() // Clear queue because we want this frame more than any others auto_cacher_->ClearSingleFrameRenders(); + DetectMulticamNode(time); + watcher->SetTicket(GetFrame(time)); } else { // There is definitely no frame here, we can immediately flip to showing nothing @@ -998,10 +1083,18 @@ bool ViewerWidget::ViewerMightBeAStill() return GetConnectedNode() && GetConnectedNode()->GetConnectedTextureOutput() && GetConnectedNode()->GetVideoLength().isNull(); } -void ViewerWidget::SetDisplayImage(QVariant frame) +void ViewerWidget::SetDisplayImage(RenderTicketPtr ticket) { foreach (ViewerDisplayWidget *dw, playback_devices_) { - dw->SetImage(frame); + QVariant push; + if (ticket) { + if (dynamic_cast(dw)) { + push = ticket->property("multicam_output"); + } else { + push = ticket->Get(); + } + } + dw->SetImage(push); } } @@ -1019,6 +1112,7 @@ RenderTicketWatcher *ViewerWidget::RequestNextFrameForQueue(bool increment) watcher = new RenderTicketWatcher(); watcher->setProperty("time", QVariant::fromValue(next_time)); + DetectMulticamNode(next_time); connect(watcher, &RenderTicketWatcher::Finished, this, &ViewerWidget::RendererGeneratedFrameForQueue); queue_watchers_.append(watcher); watcher->SetTicket(GetFrame(next_time)); @@ -1033,7 +1127,7 @@ RenderTicketPtr ViewerWidget::GetFrame(const rational &t) if (!QFileInfo::exists(cache_fn)) { // Frame hasn't been cached, start render job - return auto_cacher_->GetSingleFrame(t); + return GetSingleFrame(t); } else { // Frame has been cached, grab the frame RenderTicketPtr ticket = std::make_shared(); @@ -1160,7 +1254,7 @@ void ViewerWidget::RendererGeneratedFrame() } } - SetDisplayImage(ticket->Get()); + SetDisplayImage(ticket->GetTicket()); } } @@ -1182,7 +1276,14 @@ void ViewerWidget::RendererGeneratedFrameForQueue() rational ts = watcher->property("time").value(); foreach (ViewerDisplayWidget *dw, playback_devices_) { - dw->queue()->AppendTimewise({ts, frame}, playback_speed_); + QVariant push; + if (dynamic_cast(dw)) { + push = watcher->GetTicket()->property("multicam_output"); + } else { + push = frame; + } + + dw->queue()->AppendTimewise({ts, push}, playback_speed_); } if (prequeuing_video_) { @@ -1678,7 +1779,13 @@ void ViewerWidget::UpdateRendererVideoParameters() void ViewerWidget::UpdateRendererAudioParameters() { + AudioParams ap = GetConnectedNode()->GetAudioParams(); + UpdateAudioProcessor(); + + foreach (ViewerDisplayWidget *dw, playback_devices_) { + dw->SetAudioParams(ap); + } } void ViewerWidget::SetZoomFromMenu(QAction *action) diff --git a/app/widget/viewer/viewer.h b/app/widget/viewer/viewer.h index 4c3a18551..c465aabc9 100644 --- a/app/widget/viewer/viewer.h +++ b/app/widget/viewer/viewer.h @@ -43,6 +43,8 @@ namespace olive { +class MulticamWidget; + /** * @brief An OpenGL-based viewer widget with playback controls (a PlaybackControls widget). */ @@ -57,7 +59,9 @@ public: kWFViewerAndWaveform }; - ViewerWidget(QWidget* parent = nullptr); + ViewerWidget(QWidget* parent = nullptr) : + ViewerWidget(new ViewerDisplayWidget(), parent) + {} virtual ~ViewerWidget() override; @@ -100,6 +104,37 @@ public: enable_audio_scrubbing_ = e; } + PreviewAutoCacher *GetCacher() const { return auto_cacher_; } + + void AddPlaybackDevice(ViewerDisplayWidget *vw) + { + playback_devices_.push_back(vw); + } + + void SetTimelineSelectedBlocks(const QVector &b) + { + timeline_selected_blocks_ = b; + + if (!IsPlaying()) { + // If is playing, this will happen by the next frame automatically + DetectMulticamNode(GetTime()); + UpdateTextureFromNode(); + } + } + + void SetNodeViewSelections(const QVector &n) + { + node_view_selected_ = n; + + if (!IsPlaying()) { + // If is playing, this will happen by the next frame automatically + DetectMulticamNode(GetTime()); + UpdateTextureFromNode(); + } + } + + void ConnectMulticamWidget(MulticamWidget *p); + public slots: void Play(bool in_to_out_only); @@ -157,6 +192,8 @@ signals: void ColorManagerChanged(ColorManager* color_manager); protected: + ViewerWidget(ViewerDisplayWidget *display, QWidget* parent = nullptr); + virtual void TimebaseChangedEvent(const rational &) override; virtual void TimeChangedEvent(const rational &time) override; @@ -182,6 +219,13 @@ protected: ignore_scrub_++; } + virtual RenderTicketPtr GetSingleFrame(const rational &t, bool dry = false) + { + return auto_cacher_->GetSingleFrame(t, dry); + } + + PreviewAutoCacher *auto_cacher() const { return auto_cacher_; } + private: int64_t GetTimestamp() const { @@ -206,7 +250,7 @@ private: bool ViewerMightBeAStill(); - void SetDisplayImage(QVariant frame); + void SetDisplayImage(RenderTicketPtr ticket); RenderTicketWatcher *RequestNextFrameForQueue(bool increment = true); @@ -236,6 +280,8 @@ private: void SetWaveformMode(WaveformMode wf); + void DetectMulticamNode(const rational &time); + ViewerSizer* sizer_; int playback_speed_; @@ -304,6 +350,11 @@ private: int ignore_scrub_; + QVector timeline_selected_blocks_; + QVector node_view_selected_; + + MulticamWidget *multicam_panel_; + private slots: void PlaybackTimerUpdate(); diff --git a/app/widget/viewer/viewerdisplay.cpp b/app/widget/viewer/viewerdisplay.cpp index 70bbc51f9..e7ec99fb5 100644 --- a/app/widget/viewer/viewerdisplay.cpp +++ b/app/widget/viewer/viewerdisplay.cpp @@ -191,6 +191,15 @@ void ViewerDisplayWidget::SetVideoParams(const VideoParams ¶ms) } } +void ViewerDisplayWidget::SetAudioParams(const AudioParams ¶ms) +{ + gizmo_audio_params_ = params; + + if (gizmos_) { + update(); + } +} + void ViewerDisplayWidget::SetTime(const rational &time) { time_ = time; @@ -361,10 +370,7 @@ void ViewerDisplayWidget::OnPaint() // We only draw if we have a pipeline if (push_mode_ != kPushNull) { // Draw texture through color transform - int device_width = width() * devicePixelRatioF(); - int device_height = height() * devicePixelRatioF(); - VideoParams::Format device_format = static_cast(OLIVE_CONFIG("OfflinePixelFormat").toInt()); - VideoParams device_params(device_width, device_height, device_format, VideoParams::kInternalChannelCount); + VideoParams device_params = GetViewportParams(); if (push_mode_ == kPushBlank) { if (blank_shader_.isNull()) { @@ -392,6 +398,8 @@ void ViewerDisplayWidget::OnPaint() } else if (TexturePtr texture = load_frame_.value()) { // This is a GPU texture, switch to it directly texture_ = texture; + } else { + texture_ = LoadCustomTextureFromFrame(load_frame_); } emit TextureChanged(texture_); @@ -434,17 +442,13 @@ void ViewerDisplayWidget::OnPaint() // Draw gizmos if we have any if (gizmos_) { - NodeTraverser gt; - gt.SetCacheVideoParams(gizmo_params_); - - TimeRange range = GenerateGizmoTime(); - gizmo_db_ = gt.GenerateRow(gizmos_, range); - QPainter p(paint_device()); - gizmo_last_draw_transform_ = GenerateGizmoTransform(gt, range); + + GenerateGizmoTransforms(); + p.setWorldTransform(gizmo_last_draw_transform_); - gizmos_->UpdateGizmoPositions(gizmo_db_, NodeTraverser::GenerateGlobals(gizmo_params_, range)); + gizmos_->UpdateGizmoPositions(gizmo_db_, NodeTraverser::GenerateGlobals(gizmo_params_, gizmo_audio_params_, gizmo_draw_time_)); foreach (NodeGizmo *gizmo, gizmos_->GetGizmos()) { if (gizmo->IsVisible()) { gizmo->Draw(&p); @@ -815,18 +819,17 @@ bool ViewerDisplayWidget::OnMousePress(QMouseEvent *event) add_band_ = true; } else if (gizmos_ - && (gizmo_last_draw_transform_inverted_ = gizmo_last_draw_transform_.inverted(), - current_gizmo_ = TryGizmoPress(gizmo_db_, gizmo_last_draw_transform_inverted_.map(event->pos())))) { + && (current_gizmo_ = TryGizmoPress(gizmo_db_, gizmo_last_draw_transform_inverted_.map(event->pos())))) { // Handle gizmo click gizmo_start_drag_ = event->pos(); gizmo_last_drag_ = gizmo_start_drag_; - current_gizmo_->SetGlobals(NodeTraverser::GenerateGlobals(gizmo_params_, GenerateGizmoTime())); + current_gizmo_->SetGlobals(NodeTraverser::GenerateGlobals(gizmo_params_, gizmo_audio_params_, GenerateGizmoTime())); } else { // Handle standard drag - emit DragStarted(); + emit DragStarted(event->pos()); } @@ -874,28 +877,29 @@ bool ViewerDisplayWidget::OnMouseMove(QMouseEvent *event) // Signal movement if (DraggableGizmo *draggable = dynamic_cast(current_gizmo_)) { if (!gizmo_drag_started_) { - QPointF start = gizmo_start_drag_ * gizmo_last_draw_transform_inverted_; + QPointF start = ScreenToScenePoint(gizmo_start_drag_); rational gizmo_time = GetGizmoTime(); NodeTraverser t; t.SetCacheVideoParams(gizmo_params_); + t.SetCacheAudioParams(gizmo_audio_params_); NodeValueRow row = t.GenerateRow(gizmos_, TimeRange(gizmo_time, gizmo_time + gizmo_params_.frame_rate_as_time_base())); draggable->DragStart(row, start.x(), start.y(), gizmo_time); gizmo_drag_started_ = true; } - QPointF v = event->pos() * gizmo_last_draw_transform_inverted_; + QPointF v = ScreenToScenePoint(event->pos()); switch (draggable->GetDragValueBehavior()) { case DraggableGizmo::kAbsolute: // Above value is correct break; case DraggableGizmo::kDeltaFromPrevious: - v -= gizmo_last_drag_ * gizmo_last_draw_transform_inverted_; + v -= ScreenToScenePoint(gizmo_last_drag_); gizmo_last_drag_ = event->pos(); break; case DraggableGizmo::kDeltaFromStart: - v -= gizmo_start_drag_ * gizmo_last_draw_transform_inverted_; + v -= ScreenToScenePoint(gizmo_start_drag_); break; } @@ -1056,7 +1060,7 @@ void ViewerDisplayWidget::DrawSubtitleTracks() for (int j=subtitle_tracklist.size()-1; j>=0; j--) { Track *sub_track = subtitle_tracklist.at(j); if (!sub_track->IsMuted()) { - if (SubtitleBlock *sub = dynamic_cast(sub_track->BlockAtTime(time_))) { + if (SubtitleBlock *sub = dynamic_cast(sub_track->VisibleBlockAtTime(time_))) { // Split into lines QStringList list = QtUtils::WordWrapString(sub->GetText(), fm, bounding_box.width()); @@ -1185,6 +1189,22 @@ void ViewerDisplayWidget::CloseTextEditor() text_edit_ = nullptr; } +void ViewerDisplayWidget::GenerateGizmoTransforms() +{ + NodeTraverser gt; + gt.SetCacheVideoParams(gizmo_params_); + gt.SetCacheAudioParams(gizmo_audio_params_); + + gizmo_draw_time_ = GenerateGizmoTime(); + + if (gizmos_) { + gizmo_db_ = gt.GenerateRow(gizmos_, gizmo_draw_time_); + } + + gizmo_last_draw_transform_ = GenerateGizmoTransform(gt, gizmo_draw_time_); + gizmo_last_draw_transform_inverted_ = gizmo_last_draw_transform_.inverted(); +} + void ViewerDisplayWidget::SetShowFPS(bool e) { show_fps_ = e; @@ -1224,6 +1244,15 @@ void ViewerDisplayWidget::Pause() queue_starved_ = false; } +QPointF ViewerDisplayWidget::ScreenToScenePoint(const QPoint &p) +{ + if (gizmo_last_draw_transform_.isIdentity()) { + GenerateGizmoTransforms(); + } + + return p * gizmo_last_draw_transform_inverted_; +} + void ViewerDisplayWidget::UpdateFromQueue() { int64_t t = timer_.GetTimestampNow(); diff --git a/app/widget/viewer/viewerdisplay.h b/app/widget/viewer/viewerdisplay.h index ab39a2b10..f2e89102f 100644 --- a/app/widget/viewer/viewerdisplay.h +++ b/app/widget/viewer/viewerdisplay.h @@ -74,7 +74,13 @@ public: void SetSafeMargins(const ViewerSafeMarginInfo& safe_margin); void SetGizmos(Node* node); + + const VideoParams &GetVideoParams() const { return gizmo_params_; } void SetVideoParams(const VideoParams ¶ms); + + const AudioParams &GetAudioParams() const { return gizmo_audio_params_; } + void SetAudioParams(const AudioParams &p); + void SetTime(const rational& time); void SetSubtitleTracks(Sequence *list); @@ -131,6 +137,8 @@ public: return &timer_; } + QPointF ScreenToScenePoint(const QPoint &p); + virtual bool eventFilter(QObject *o, QEvent *e) override; public slots: @@ -182,7 +190,7 @@ signals: /** * @brief Signal emitted when the user starts dragging from the viewer */ - void DragStarted(); + void DragStarted(const QPoint &p); /** * @brief Signal emitted when a hand drag starts @@ -218,6 +226,30 @@ signals: void CreateAddableAt(const QRectF &rect); +protected: + QTransform GenerateWorldTransform(); + + QTransform GenerateDisplayTransform(); + + QTransform GenerateGizmoTransform(NodeTraverser >, const TimeRange &range); + QTransform GenerateGizmoTransform() + { + NodeTraverser t; + t.SetCacheVideoParams(gizmo_params_); + return GenerateGizmoTransform(t, GenerateGizmoTime()); + } + + TimeRange GenerateGizmoTime() + { + rational node_time = GetGizmoTime(); + return TimeRange(node_time, node_time + gizmo_params_.frame_rate_as_time_base()); + } + + virtual TexturePtr LoadCustomTextureFromFrame(const QVariant &v) + { + return nullptr; + } + protected slots: /** * @brief Paint function to display the texture (received in SetTexture()) on screen. @@ -241,24 +273,6 @@ private: void UpdateMatrix(); - QTransform GenerateWorldTransform(); - - QTransform GenerateDisplayTransform(); - - QTransform GenerateGizmoTransform(NodeTraverser >, const TimeRange &range); - QTransform GenerateGizmoTransform() - { - NodeTraverser t; - t.SetCacheVideoParams(gizmo_params_); - return GenerateGizmoTransform(t, GenerateGizmoTime()); - } - - TimeRange GenerateGizmoTime() - { - rational node_time = GetGizmoTime(); - return TimeRange(node_time, node_time + gizmo_params_.frame_rate_as_time_base()); - } - NodeGizmo *TryGizmoPress(const NodeValueRow &row, const QPointF &p); void OpenTextGizmo(TextGizmo *text, QMouseEvent *event = nullptr); @@ -290,6 +304,8 @@ private: void CloseTextEditor(); + void GenerateGizmoTransforms(); + /** * @brief Internal reference to the OpenGL texture to draw. Set in SetTexture() and used in paintGL(). */ @@ -338,8 +354,10 @@ private: Node* gizmos_; NodeValueRow gizmo_db_; VideoParams gizmo_params_; + AudioParams gizmo_audio_params_; QPoint gizmo_start_drag_; QPoint gizmo_last_drag_; + TimeRange gizmo_draw_time_; NodeGizmo *current_gizmo_; bool gizmo_drag_started_; QTransform gizmo_last_draw_transform_; diff --git a/app/widget/viewer/viewerqueue.h b/app/widget/viewer/viewerqueue.h index f8117eac9..053d8f013 100644 --- a/app/widget/viewer/viewerqueue.h +++ b/app/widget/viewer/viewerqueue.h @@ -27,12 +27,14 @@ namespace olive { -struct ViewerPlaybackFrame { +struct ViewerPlaybackFrame +{ rational timestamp; QVariant frame; }; -class ViewerQueue : public std::list { +class ViewerQueue : public std::list +{ public: ViewerQueue() = default; diff --git a/app/widget/viewer/viewersizer.cpp b/app/widget/viewer/viewersizer.cpp index 7041da533..d8ae52b84 100644 --- a/app/widget/viewer/viewersizer.cpp +++ b/app/widget/viewer/viewersizer.cpp @@ -39,6 +39,8 @@ ViewerSizer::ViewerSizer(QWidget *parent) : vert_scrollbar_ = new QScrollBar(Qt::Vertical, this); vert_scrollbar_->setVisible(false); connect(vert_scrollbar_, &QScrollBar::valueChanged, this, &ViewerSizer::ScrollBarMoved); + + setSizePolicy(QSizePolicy::Expanding, QSizePolicy::Expanding); } void ViewerSizer::SetWidget(QWidget *widget) diff --git a/app/widget/viewer/viewersizer.h b/app/widget/viewer/viewersizer.h index 7acb76965..b4afe5015 100644 --- a/app/widget/viewer/viewersizer.h +++ b/app/widget/viewer/viewersizer.h @@ -51,6 +51,7 @@ public: */ void SetWidget(QWidget* widget); +public slots: /** * @brief Set resolution to use * @@ -70,7 +71,6 @@ public: */ void SetZoom(int percent); -public slots: void HandDragMove(int x, int y); signals: diff --git a/app/window/mainwindow/mainwindow.cpp b/app/window/mainwindow/mainwindow.cpp index 5b76ed1b0..79b589901 100644 --- a/app/window/mainwindow/mainwindow.cpp +++ b/app/window/mainwindow/mainwindow.cpp @@ -87,6 +87,7 @@ MainWindow::MainWindow(QWidget *parent) : param_panel_ = new ParamPanel(this); curve_panel_ = new CurvePanel(this); sequence_viewer_panel_ = new SequenceViewerPanel(this); + multicam_panel_ = new MulticamPanel(this); pixel_sampler_panel_ = new PixelSamplerPanel(this); AppendProjectPanel(); tool_panel_ = new ToolPanel(this); @@ -104,18 +105,22 @@ MainWindow::MainWindow(QWidget *parent) : connect(param_panel_, &ParamPanel::FocusedNodeChanged, curve_panel_, &CurvePanel::SetNode); connect(param_panel_, &ParamPanel::SelectedNodesChanged, node_panel_, &NodePanel::Select); - // Connect time signals together - connect(sequence_viewer_panel_, &SequenceViewerPanel::TimeChanged, param_panel_, &ParamPanel::SetTime); - connect(sequence_viewer_panel_, &SequenceViewerPanel::TimeChanged, curve_panel_, &NodeTablePanel::SetTime); - connect(param_panel_, &ParamPanel::TimeChanged, sequence_viewer_panel_, &SequenceViewerPanel::SetTime); - connect(param_panel_, &ParamPanel::TimeChanged, curve_panel_, &NodeTablePanel::SetTime); - connect(curve_panel_, &ParamPanel::TimeChanged, sequence_viewer_panel_, &SequenceViewerPanel::SetTime); - connect(curve_panel_, &ParamPanel::TimeChanged, param_panel_, &NodeTablePanel::SetTime); + connect(node_panel_, &NodePanel::NodeSelectionChanged, sequence_viewer_panel_, &ViewerPanel::SetNodeViewSelections); - connect(PanelManager::instance(), &PanelManager::FocusedPanelChanged, this, &MainWindow::FocusedPanelChanged); + // Connect time signals together + AddMainTimePanel(multicam_panel_); + AddMainTimePanel(curve_panel_); + AddMainTimePanel(param_panel_); + AddMainTimePanel(sequence_viewer_panel_); sequence_viewer_panel_->ConnectTimeBasedPanel(param_panel_); sequence_viewer_panel_->ConnectTimeBasedPanel(curve_panel_); + sequence_viewer_panel_->ConnectTimeBasedPanel(multicam_panel_); + + connect(PanelManager::instance(), &PanelManager::FocusedPanelChanged, this, &MainWindow::FocusedPanelChanged); + + sequence_viewer_panel_->AddPlaybackDevice(multicam_panel_->GetMulticamWidget()->GetDisplayWidget()); + sequence_viewer_panel_->ConnectMulticamWidget(multicam_panel_->GetMulticamWidget()); scope_panel_->SetViewerPanel(sequence_viewer_panel_); @@ -489,6 +494,7 @@ void MainWindow::TimelinePanelSelectionChanged(const QVector &blocks) if (PanelManager::instance()->CurrentlyFocused(false) == panel) { UpdateNodePanelContextFromTimelinePanel(panel); + sequence_viewer_panel_->SetTimelineSelectedBlocks(blocks); } } @@ -591,21 +597,32 @@ void MainWindow::FloatingPanelCloseRequested() panel->deleteLater(); } +void MainWindow::AddMainTimePanel(TimeBasedPanel *p) +{ + main_time_panels_.append(p); + connect(p, &TimeBasedPanel::TimeChanged, this, &MainWindow::UpdateMainTimePanels); +} + +void MainWindow::UpdateMainTimePanels(const rational &r) +{ + for (TimeBasedPanel *p : main_time_panels_) { + if (p != sender()) { + p->SetTime(r); + } + } +} + TimelinePanel* MainWindow::AppendTimelinePanel() { TimelinePanel* panel = AppendPanelInternal(timeline_panels_); connect(panel, &PanelWidget::CloseRequested, this, &MainWindow::TimelineCloseRequested); - connect(panel, &TimelinePanel::TimeChanged, curve_panel_, &ParamPanel::SetTime); - connect(panel, &TimelinePanel::TimeChanged, param_panel_, &ParamPanel::SetTime); - connect(panel, &TimelinePanel::TimeChanged, sequence_viewer_panel_, &SequenceViewerPanel::SetTime); connect(panel, &TimelinePanel::RequestCaptureStart, sequence_viewer_panel_, &SequenceViewerPanel::StartCapture); connect(panel, &TimelinePanel::BlockSelectionChanged, this, &MainWindow::TimelinePanelSelectionChanged); connect(panel, &TimelinePanel::RevealViewerInProject, this, &MainWindow::RevealViewerInProject); connect(panel, &TimelinePanel::RevealViewerInFootageViewer, this, &MainWindow::RevealViewerInFootageViewer); - connect(param_panel_, &ParamPanel::TimeChanged, panel, &TimelinePanel::SetTime); - connect(curve_panel_, &ParamPanel::TimeChanged, panel, &TimelinePanel::SetTime); - connect(sequence_viewer_panel_, &SequenceViewerPanel::TimeChanged, panel, &TimelinePanel::SetTime); + + AddMainTimePanel(panel); sequence_viewer_panel_->ConnectTimeBasedPanel(panel); @@ -647,6 +664,7 @@ void MainWindow::RemoveProjectPanel(ProjectPanel *panel) void MainWindow::TimelineFocused(ViewerOutput* viewer) { sequence_viewer_panel_->ConnectViewerNode(viewer); + multicam_panel_->ConnectViewerNode(viewer); param_panel_->ConnectViewerNode(viewer); curve_panel_->ConnectViewerNode(viewer); } @@ -837,6 +855,10 @@ void MainWindow::SetDefaultLayout() scope_panel_->setFloating(true); addDockWidget(Qt::TopDockWidgetArea, scope_panel_); + multicam_panel_->hide(); + multicam_panel_->setFloating(true); + addDockWidget(Qt::TopDockWidgetArea, multicam_panel_); + sequence_viewer_panel_->show(); addDockWidget(Qt::TopDockWidgetArea, sequence_viewer_panel_); diff --git a/app/window/mainwindow/mainwindow.h b/app/window/mainwindow/mainwindow.h index 1f6504c6f..7f319e4b1 100644 --- a/app/window/mainwindow/mainwindow.h +++ b/app/window/mainwindow/mainwindow.h @@ -25,6 +25,7 @@ #include "mainwindowlayoutinfo.h" #include "node/project/project.h" +#include "panel/multicam/multicampanel.h" #include "panel/panelmanager.h" #include "panel/audiomonitor/audiomonitor.h" #include "panel/curve/curve.h" @@ -146,6 +147,8 @@ private: void SelectFootageForProjectPanel(const QVector &e, ProjectPanel *p); + void AddMainTimePanel(TimeBasedPanel *p); + QByteArray premaximized_state_; // Standard panels @@ -163,6 +166,7 @@ private: PixelSamplerPanel* pixel_sampler_panel_; ScopePanel* scope_panel_; QMap viewer_panels_; + MulticamPanel *multicam_panel_; #ifdef Q_OS_WINDOWS unsigned int taskbar_btn_id_; @@ -172,6 +176,8 @@ private: bool first_show_; + QVector main_time_panels_; + private slots: void FocusedPanelChanged(PanelWidget* panel); @@ -202,6 +208,8 @@ private slots: void RevealViewerInProject(ViewerOutput *r); void RevealViewerInFootageViewer(ViewerOutput *r, const TimeRange &range); + void UpdateMainTimePanels(const rational &r); + }; }