diff --git a/app/node/node.h b/app/node/node.h index 5c848100c..17aefb72a 100644 --- a/app/node/node.h +++ b/app/node/node.h @@ -212,9 +212,38 @@ public: return input_ids_; } - virtual bool IsInputActiveAtTime(const QString &input, int element, const TimeRange &r) const + class ActiveElements { - return true; + 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 diff --git a/app/node/output/track/track.cpp b/app/node/output/track/track.cpp index b2d3775f0..b979357d6 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,51 @@ 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()) { + a.add(GetArrayIndexFromCacheIndex(i)); + } + } + + 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(table, globals.time()); + } +} + TimeRange Track::InputTimeAdjustment(const QString& input, int element, const TimeRange& input_time) const { if (input == kBlockInput && element >= 0) { @@ -352,58 +399,6 @@ Block *Track::NearestBlockAfter(const rational &time) const return nullptr; } -Block *Track::BlockAtTime(const rational &time) const -{ - if (IsMuted() || time > track_length() || blocks_.isEmpty()) { - return nullptr; - } - - // Use binary search to find block at time - Block* using_block = nullptr; - - 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; - break; - } else if (block->out() <= time) { - low = mid + 1; - } else { - high = mid - 1; - } - } - - 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; -} - void Track::InvalidateCache(const TimeRange& range, const QString& from, int element, InvalidateCacheOptions options) { TimeRange limited; @@ -579,6 +574,127 @@ 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(NodeValueTable *table, const TimeRange &range) const +{ + /* + // 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; 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..e2ac8d3bb 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,30 +316,6 @@ 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. - */ - 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; - const QVector &Blocks() const { return blocks_; @@ -345,6 +323,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 +451,10 @@ private: int GetCacheIndexFromArrayIndex(int index) const; + int GetBlockIndexAtTime(const rational &time) const; + + void ProcessAudioTrack(NodeValueTable *table, const TimeRange &range) const; + TimeRangeList block_length_pending_invalidations_; QVector blocks_; diff --git a/app/node/traverser.cpp b/app/node/traverser.cpp index 712aa798c..8fab59bb9 100644 --- a/app/node/traverser.cpp +++ b/app/node/traverser.cpp @@ -195,10 +195,6 @@ TexturePtr NodeTraverser::GetMainTextureFromJob(const GenerateJob &job) NodeValueTable NodeTraverser::ProcessInput(const Node* node, const QString& input, const TimeRange& range) { - if (!node->IsInputActiveAtTime(input, -1, range)) { - return NodeValueTable(); - } - // If input is connected, retrieve value directly if (node->IsInputConnected(input)) { @@ -220,19 +216,15 @@ NodeValueTable NodeTraverser::ProcessInput(const Node* node, const QString& inpu // Value is an array, we will return a list of NodeValueTables NodeValueTableArray array_tbl; - int sz = node->InputArraySize(input); - for (int i=0; iIsInputActiveAtTime(input, i, range)) { - NodeValueTable& sub_tbl = array_tbl[i]; - TimeRange adjusted_range = node->InputTimeAdjustment(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), @@ -278,12 +284,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); - } - // FIXME: Cache certain values here if we've already processed them before // Generate row for node @@ -338,22 +338,6 @@ NodeValueTable NodeTraverser::GenerateTable(const Node *n, const TimeRange& rang } } -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 bde1733ae..26c9afbcf 100644 --- a/app/node/traverser.h +++ b/app/node/traverser.h @@ -87,7 +87,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/render/renderprocessor.cpp b/app/render/renderprocessor.cpp index 426b0a9ed..84c4ec67f 100644 --- a/app/render/renderprocessor.cpp +++ b/app/render/renderprocessor.cpp @@ -300,112 +300,6 @@ void RenderProcessor::Process(RenderTicketPtr ticket, Renderer *render_ctx, Deco 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 a537ccf89..6dc02f04c 100644 --- a/app/render/renderprocessor.h +++ b/app/render/renderprocessor.h @@ -42,8 +42,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/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/viewer/viewerdisplay.cpp b/app/widget/viewer/viewerdisplay.cpp index 70bbc51f9..23d41efb1 100644 --- a/app/widget/viewer/viewerdisplay.cpp +++ b/app/widget/viewer/viewerdisplay.cpp @@ -1056,7 +1056,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());