build: split the engine into liboakengine.so; worker drops the UI entirely
Physical split: app/{audio,cli,codec,common,config,node,pluginSupport,
render,task,timeline,undo,tool,shaders} plus coreengine, version and
ui/icons+colorcoding move to a new top-level engine/ tree, built as
liboakengine.so (shared). The render backends (oakgl/oakvulkan) move
with it and link the engine library instead of embedding a static
render-core subset (libolive-rendercore is gone).
- oak-render-worker now links liboakengine instead of the whole
libolive-editor object set: 336MB -> 2.9MB, no Qt Widgets UI
- the editor links liboakengine for the engine and keeps only UI
objects in libolive-editor
- install/packaging: GNUInstallDirs libdir on Linux, bundle copy on
macOS, oakengine.dll staged for NSIS, AppImage validation entry
- fix backend lookup for the new layout: DynamicRenderer searched
../app but backends now live in engine/; a stale pre-split liboakgl
in the build tree got dlopened instead, re-initialized and later
destroyed the interposed engine statics (full-suite segfault at
DialogSequenceParameterTab, found via gdb watchpoint)
This commit is contained in:
@@ -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 <http://www.gnu.org/licenses/>.
|
||||
|
||||
set(OLIVE_SOURCES
|
||||
${OLIVE_SOURCES}
|
||||
node/output/track/track.cpp
|
||||
node/output/track/track.h
|
||||
node/output/track/tracklist.cpp
|
||||
node/output/track/tracklist.h
|
||||
PARENT_SCOPE
|
||||
)
|
||||
@@ -0,0 +1,881 @@
|
||||
/***
|
||||
|
||||
Olive - Non-Linear Video Editor
|
||||
Copyright (C) 2022 Olive Team
|
||||
Modifications Copyright (C) 2025 mikesolar
|
||||
|
||||
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 <http://www.gnu.org/licenses/>.
|
||||
|
||||
***/
|
||||
|
||||
#include "track.h"
|
||||
|
||||
#include <QDebug>
|
||||
#include <QFontMetrics>
|
||||
|
||||
#include "audio/audioprocessor.h"
|
||||
#include "node/block/clip/clip.h"
|
||||
#include "node/block/gap/gap.h"
|
||||
#include "node/block/transition/transition.h"
|
||||
|
||||
namespace olive
|
||||
{
|
||||
|
||||
#define super Node
|
||||
|
||||
const double Track::k_track_height_default = 3.0;
|
||||
const double Track::k_track_height_minimum = 1.5;
|
||||
const double Track::k_track_height_interval = 0.5;
|
||||
|
||||
const QString Track::k_block_input = QStringLiteral("block_in");
|
||||
const QString Track::k_muted_input = QStringLiteral("muted_in");
|
||||
const QString Track::k_array_map_input = QStringLiteral("arraymap_in");
|
||||
|
||||
Track::Track()
|
||||
: track_type_(Track::k_none)
|
||||
, index_(-1)
|
||||
, locked_(false)
|
||||
, sequence_(nullptr)
|
||||
, ignore_arraymap_(0)
|
||||
, arraymap_invalid_(false)
|
||||
, ignore_arraymap_set_(false)
|
||||
{
|
||||
add_input(k_block_input, NodeValue::k_none,
|
||||
InputFlags(k_input_flag_array | k_input_flag_not_keyframable |
|
||||
k_input_flag_hidden | k_input_flag_ignore_invalidations));
|
||||
|
||||
add_input(k_muted_input, NodeValue::k_boolean, false,
|
||||
InputFlags(k_input_flag_not_connectable | k_input_flag_not_keyframable));
|
||||
|
||||
add_input(k_array_map_input, NodeValue::k_binary,
|
||||
InputFlags(k_input_flag_static | k_input_flag_hidden |
|
||||
k_input_flag_ignore_invalidations));
|
||||
|
||||
// Set default height
|
||||
track_height_ = k_track_height_default;
|
||||
}
|
||||
|
||||
void Track::set_type(const Type &track_type)
|
||||
{
|
||||
track_type_ = track_type;
|
||||
}
|
||||
|
||||
const Track::Type &Track::type() const
|
||||
{
|
||||
return track_type_;
|
||||
}
|
||||
|
||||
QString Track::name() const
|
||||
{
|
||||
if (track_type_ == Track::k_video) {
|
||||
return tr("Video Track %1").arg(index_);
|
||||
} else if (track_type_ == Track::k_audio) {
|
||||
return tr("Audio Track %1").arg(index_);
|
||||
} else if (track_type_ == Track::k_subtitle) {
|
||||
return tr("Subtitle Track %1").arg(index_);
|
||||
}
|
||||
|
||||
return tr("Track");
|
||||
}
|
||||
|
||||
QString Track::id() const
|
||||
{
|
||||
return QStringLiteral("org.olivevideoeditor.Olive.track");
|
||||
}
|
||||
|
||||
QVector<Node::CategoryID> Track::category() const
|
||||
{
|
||||
return { k_category_timeline };
|
||||
}
|
||||
|
||||
QString Track::description() const
|
||||
{
|
||||
return tr(
|
||||
"Node for representing and processing a single array of Blocks sorted by time. Also represents the end of "
|
||||
"a Sequence.");
|
||||
}
|
||||
|
||||
Node::ActiveElements Track::get_active_elements_at_time(const QString &input,
|
||||
const TimeRange &r) const
|
||||
{
|
||||
if (input == k_block_input) {
|
||||
if (is_muted() || blocks_.empty() || r.in() >= track_length() ||
|
||||
r.out() <= 0) {
|
||||
return ActiveElements::k_no_elements;
|
||||
} else {
|
||||
int start = get_block_index_at_time(r.in());
|
||||
int end = get_block_index_at_time(r.out());
|
||||
|
||||
if (start == -1) {
|
||||
start = 0;
|
||||
}
|
||||
if (end == -1) {
|
||||
end = blocks_.size() - 1;
|
||||
}
|
||||
|
||||
if (blocks_.at(end)->in() == r.out()) {
|
||||
end--;
|
||||
}
|
||||
|
||||
ActiveElements a;
|
||||
for (int i = start; i <= end; i++) {
|
||||
Block *b = blocks_.at(i);
|
||||
if (b->is_enabled() && (dynamic_cast<ClipBlock *>(b) ||
|
||||
dynamic_cast<TransitionBlock *>(b))) {
|
||||
a.add(get_array_index_from_cache_index(i));
|
||||
}
|
||||
}
|
||||
|
||||
if (a.elements().empty()) {
|
||||
return ActiveElements::k_no_elements;
|
||||
} else {
|
||||
return a;
|
||||
}
|
||||
}
|
||||
} else {
|
||||
return super::get_active_elements_at_time(input, r);
|
||||
}
|
||||
}
|
||||
|
||||
void Track::value(const NodeValueRow &value, const NodeGlobals &globals,
|
||||
NodeValueTable *table) const
|
||||
{
|
||||
if (this->type() == Track::k_video) {
|
||||
// Just pass straight through
|
||||
NodeValueArray a = value[k_block_input].to_array();
|
||||
if (!a.empty()) {
|
||||
table->push(a.begin()->second);
|
||||
}
|
||||
} else if (this->type() == Track::k_audio) {
|
||||
// Audio
|
||||
process_audio_track(value, globals, table);
|
||||
}
|
||||
}
|
||||
|
||||
TimeRange Track::input_time_adjustment(const QString &input, int element,
|
||||
const TimeRange &input_time,
|
||||
bool clamp) const
|
||||
{
|
||||
if (input == k_block_input && element >= 0) {
|
||||
int cache_index = get_cache_index_from_array_index(element);
|
||||
|
||||
if (cache_index > -1) {
|
||||
TimeRange r = input_time;
|
||||
Block *b = blocks_.at(cache_index);
|
||||
|
||||
if (clamp) {
|
||||
r.set_range(std::max(r.in(), b->in()),
|
||||
std::min(r.out(), b->out()));
|
||||
}
|
||||
|
||||
return transform_range_for_block(b, r);
|
||||
}
|
||||
}
|
||||
|
||||
return Node::input_time_adjustment(input, element, input_time, clamp);
|
||||
}
|
||||
|
||||
TimeRange Track::output_time_adjustment(const QString &input, int element,
|
||||
const TimeRange &input_time) const
|
||||
{
|
||||
if (input == k_block_input && element >= 0) {
|
||||
int cache_index = get_cache_index_from_array_index(element);
|
||||
|
||||
if (cache_index > -1) {
|
||||
return transform_range_from_block(blocks_.at(cache_index), input_time);
|
||||
}
|
||||
}
|
||||
|
||||
return Node::output_time_adjustment(input, element, input_time);
|
||||
}
|
||||
|
||||
const double &Track::get_track_height() const
|
||||
{
|
||||
return track_height_;
|
||||
}
|
||||
|
||||
void Track::set_track_height(const double &height)
|
||||
{
|
||||
track_height_ = height;
|
||||
emit track_height_changed(track_height_);
|
||||
}
|
||||
|
||||
bool Track::load_custom(QXmlStreamReader *reader, SerializedData *data)
|
||||
{
|
||||
ignore_arraymap_set_ = true;
|
||||
|
||||
while (xml_read_next_start_element(reader)) {
|
||||
if (reader->name() == QStringLiteral("height")) {
|
||||
this->set_track_height(reader->readElementText().toDouble());
|
||||
} else {
|
||||
reader->skipCurrentElement();
|
||||
}
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
void Track::save_custom(QXmlStreamWriter *writer) const
|
||||
{
|
||||
writer->writeTextElement(QStringLiteral("height"),
|
||||
QString::number(this->get_track_height()));
|
||||
}
|
||||
|
||||
void Track::PostLoadEvent(SerializedData *data)
|
||||
{
|
||||
ignore_arraymap_set_ = false;
|
||||
refresh_block_cache_from_array_map();
|
||||
}
|
||||
|
||||
void Track::InputValueChangedEvent(const QString &input, int element)
|
||||
{
|
||||
Q_UNUSED(element)
|
||||
|
||||
if (input == k_muted_input) {
|
||||
emit muted_changed(is_muted());
|
||||
} else if (input == k_array_map_input) {
|
||||
if (ignore_arraymap_ > 0) {
|
||||
ignore_arraymap_--;
|
||||
} else {
|
||||
refresh_block_cache_from_array_map();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
void Track::retranslate()
|
||||
{
|
||||
super::retranslate();
|
||||
|
||||
set_input_name(k_block_input, tr("Blocks"));
|
||||
set_input_name(k_muted_input, tr("Muted"));
|
||||
}
|
||||
|
||||
void Track::set_index(const int &index)
|
||||
{
|
||||
int old = index_;
|
||||
|
||||
index_ = index;
|
||||
|
||||
emit index_changed(old, index_);
|
||||
}
|
||||
|
||||
Block *Track::block_containing_time(const Rational &time) const
|
||||
{
|
||||
foreach (Block *block, blocks_) {
|
||||
if (block->in() < time && block->out() > time) {
|
||||
return block;
|
||||
} else if (block->out() == time) {
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
return nullptr;
|
||||
}
|
||||
|
||||
Block *Track::nearest_block_before(const Rational &time) const
|
||||
{
|
||||
foreach (Block *block, blocks_) {
|
||||
// Blocks are sorted by time, so the first Block who's out point is at/after this time is the correct Block
|
||||
if (block->in() == time) {
|
||||
break;
|
||||
}
|
||||
|
||||
if (block->out() >= time) {
|
||||
return block;
|
||||
}
|
||||
}
|
||||
|
||||
return nullptr;
|
||||
}
|
||||
|
||||
Block *Track::nearest_block_before_or_at(const Rational &time) const
|
||||
{
|
||||
foreach (Block *block, blocks_) {
|
||||
// Blocks are sorted by time, so the first Block who's out point is at/after this time is the correct Block
|
||||
if (block->out() > time) {
|
||||
return block;
|
||||
}
|
||||
}
|
||||
|
||||
return nullptr;
|
||||
}
|
||||
|
||||
Block *Track::nearest_block_after_or_at(const Rational &time) const
|
||||
{
|
||||
foreach (Block *block, blocks_) {
|
||||
// Blocks are sorted by time, so the first Block after this time is the correct Block
|
||||
if (block->in() >= time) {
|
||||
return block;
|
||||
}
|
||||
}
|
||||
|
||||
return nullptr;
|
||||
}
|
||||
|
||||
Block *Track::nearest_block_after(const Rational &time) const
|
||||
{
|
||||
foreach (Block *block, blocks_) {
|
||||
// Blocks are sorted by time, so the first Block after this time is the correct Block
|
||||
if (block->in() > time) {
|
||||
return block;
|
||||
}
|
||||
}
|
||||
|
||||
return nullptr;
|
||||
}
|
||||
|
||||
bool Track::is_range_free(const TimeRange &range) const
|
||||
{
|
||||
Block *b = nearest_block_before_or_at(range.in());
|
||||
if (!b) {
|
||||
// No block here, assume track is empty here
|
||||
return true;
|
||||
}
|
||||
|
||||
if (!dynamic_cast<GapBlock *>(b)) {
|
||||
// There's a block at or around the start point that isn't a gap, range is not free
|
||||
return false;
|
||||
}
|
||||
|
||||
while ((b = b->next())) {
|
||||
if (b->in() >= range.out()) {
|
||||
// This block is after the range, no longer relevant
|
||||
break;
|
||||
} else if (!dynamic_cast<GapBlock *>(b)) {
|
||||
// Found a block in this range, range is not free
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
// If we get here, we couldn't find anything in the way of this range
|
||||
return true;
|
||||
}
|
||||
|
||||
void Track::invalidate_cache(const TimeRange &range, const QString &from,
|
||||
int element, InvalidateCacheOptions options)
|
||||
{
|
||||
TimeRange limited;
|
||||
|
||||
const Block *b;
|
||||
|
||||
if (from == k_block_input && element >= 0 &&
|
||||
(b = dynamic_cast<const Block *>(get_connected_output(from, element))) &&
|
||||
!options.value(QStringLiteral("lengthevent")).toBool()) {
|
||||
// Limit the range signal to the corresponding block
|
||||
TimeRange transformed = transform_range_from_block(b, range);
|
||||
|
||||
if (transformed.out() <= b->in() || transformed.in() >= b->out()) {
|
||||
return;
|
||||
}
|
||||
|
||||
limited = TimeRange(qMax(transformed.in(), b->in()),
|
||||
qMin(transformed.out(), b->out()));
|
||||
} else {
|
||||
limited = range;
|
||||
}
|
||||
|
||||
// NOTE: For now, I figure we drop this key, but we may find in the future that it's advantageous
|
||||
// to keep it
|
||||
options.remove(QStringLiteral("lengthevent"));
|
||||
|
||||
Node::invalidate_cache(limited, from, element, options);
|
||||
}
|
||||
|
||||
void Track::insert_block_before(Block *block, Block *after)
|
||||
{
|
||||
if (!after) {
|
||||
append_block(block);
|
||||
} else {
|
||||
insert_block_at_index(block, blocks_.indexOf(after));
|
||||
}
|
||||
}
|
||||
|
||||
void Track::insert_block_after(Block *block, Block *before)
|
||||
{
|
||||
if (!before) {
|
||||
prepend_block(block);
|
||||
} else {
|
||||
int before_index = blocks_.indexOf(before);
|
||||
|
||||
Q_ASSERT(before_index >= 0);
|
||||
|
||||
insert_block_at_index(block, before_index + 1);
|
||||
}
|
||||
}
|
||||
|
||||
void Track::prepend_block(Block *block)
|
||||
{
|
||||
insert_block_at_index(block, 0);
|
||||
}
|
||||
|
||||
void Track::insert_block_at_index(Block *block, int index)
|
||||
{
|
||||
// Set track
|
||||
Q_ASSERT(block->track() == nullptr);
|
||||
block->set_track(this);
|
||||
|
||||
// Update array
|
||||
int array_index = connect_block(block);
|
||||
blocks_.insert(index, block);
|
||||
block_array_indexes_.insert(index, array_index);
|
||||
|
||||
// Handle previous/next
|
||||
Block *previous = (index > 0) ? blocks_.at(index - 1) : nullptr;
|
||||
Block *next = (index < blocks_.size() - 1) ? blocks_.at(index + 1) :
|
||||
nullptr;
|
||||
Block::set_previous_next(previous, block);
|
||||
Block::set_previous_next(block, next);
|
||||
|
||||
// Update in/out
|
||||
update_in_out_from(index);
|
||||
|
||||
connect(block, &Block::length_changed, this, &Track::block_length_changed);
|
||||
|
||||
Node::invalidate_cache(TimeRange(block->in(), track_length()), k_block_input);
|
||||
|
||||
emit block_added(block);
|
||||
|
||||
update_array_map();
|
||||
}
|
||||
|
||||
void Track::append_block(Block *block)
|
||||
{
|
||||
insert_block_at_index(block, blocks_.size());
|
||||
}
|
||||
|
||||
void Track::ripple_remove_block(Block *block)
|
||||
{
|
||||
Rational remove_in = block->in();
|
||||
Rational remove_out = block->out();
|
||||
|
||||
emit block_removed(block);
|
||||
|
||||
// Set track
|
||||
Q_ASSERT(block->track() == this);
|
||||
block->set_track(nullptr);
|
||||
|
||||
// Update array
|
||||
int index = blocks_.indexOf(block);
|
||||
Q_ASSERT(index != -1);
|
||||
|
||||
int array_index = block_array_indexes_.at(index);
|
||||
|
||||
blocks_.removeAt(index);
|
||||
block_array_indexes_.removeAt(index);
|
||||
|
||||
Node::disconnect_edge(block, NodeInput(this, k_block_input, array_index));
|
||||
empty_inputs_.push_back(array_index);
|
||||
disconnect(block, &Block::length_changed, this, &Track::block_length_changed);
|
||||
|
||||
// Handle previous/next
|
||||
Block *previous = (index > 0) ? blocks_.at(index - 1) : nullptr;
|
||||
Block *next = (index < blocks_.size()) ? blocks_.at(index) : nullptr;
|
||||
Block::set_previous_next(previous, next);
|
||||
block->set_previous(nullptr);
|
||||
block->set_next(nullptr);
|
||||
block->set_in(0);
|
||||
block->set_out(block->length());
|
||||
|
||||
// Update in/outs
|
||||
update_in_out_from(index);
|
||||
|
||||
Node::invalidate_cache(
|
||||
TimeRange(remove_in, qMax(track_length(), remove_out)), k_block_input);
|
||||
|
||||
update_array_map();
|
||||
}
|
||||
|
||||
void Track::replace_block(Block *old, Block *replace)
|
||||
{
|
||||
emit block_removed(old);
|
||||
|
||||
// Set track
|
||||
Q_ASSERT(old->track() == this);
|
||||
old->set_track(nullptr);
|
||||
|
||||
Q_ASSERT(replace->track() == nullptr);
|
||||
replace->set_track(this);
|
||||
|
||||
// Update array
|
||||
int cache_index = blocks_.indexOf(old);
|
||||
int index_of_old_block = get_array_index_from_cache_index(cache_index);
|
||||
|
||||
disconnect_edge(old, NodeInput(this, k_block_input, index_of_old_block));
|
||||
connect_edge(replace, NodeInput(this, k_block_input, index_of_old_block));
|
||||
blocks_.replace(cache_index, replace);
|
||||
disconnect(old, &Block::length_changed, this, &Track::block_length_changed);
|
||||
connect(replace, &Block::length_changed, this, &Track::block_length_changed);
|
||||
|
||||
// Handle previous/next
|
||||
replace->set_previous(old->previous());
|
||||
replace->set_next(old->next());
|
||||
old->set_previous(nullptr);
|
||||
old->set_next(nullptr);
|
||||
if (replace->previous()) {
|
||||
replace->previous()->set_next(replace);
|
||||
}
|
||||
if (replace->next()) {
|
||||
replace->next()->set_previous(replace);
|
||||
}
|
||||
|
||||
if (old->length() == replace->length()) {
|
||||
replace->set_in(replace->previous() ? replace->previous()->out() : 0);
|
||||
replace->set_out(replace->in() + replace->length());
|
||||
|
||||
Node::invalidate_cache(TimeRange(replace->in(), replace->out()),
|
||||
k_block_input);
|
||||
} else {
|
||||
// Update in/outs
|
||||
update_in_out_from(cache_index);
|
||||
|
||||
Node::invalidate_cache(TimeRange(replace->in(), track_length()),
|
||||
k_block_input);
|
||||
}
|
||||
|
||||
emit block_added(replace);
|
||||
|
||||
update_array_map();
|
||||
}
|
||||
|
||||
Rational Track::track_length() const
|
||||
{
|
||||
if (blocks_.isEmpty()) {
|
||||
return 0;
|
||||
} else {
|
||||
return blocks_.last()->out();
|
||||
}
|
||||
}
|
||||
|
||||
bool Track::is_muted() const
|
||||
{
|
||||
return get_standard_value(k_muted_input).toBool();
|
||||
}
|
||||
|
||||
bool Track::is_locked() const
|
||||
{
|
||||
return locked_;
|
||||
}
|
||||
|
||||
void Track::set_muted(bool e)
|
||||
{
|
||||
set_standard_value(k_muted_input, e);
|
||||
}
|
||||
|
||||
void Track::set_locked(bool e)
|
||||
{
|
||||
locked_ = e;
|
||||
}
|
||||
|
||||
void Track::InputConnectedEvent(const QString &input, int element, Node *node)
|
||||
{
|
||||
if (arraymap_invalid_ && input == k_block_input && element >= 0) {
|
||||
refresh_block_cache_from_array_map();
|
||||
}
|
||||
}
|
||||
|
||||
void Track::update_in_out_from(int index)
|
||||
{
|
||||
// Find block just before this one to find the last out point
|
||||
Rational last_out = (index == 0) ? 0 : blocks_.at(index - 1)->out();
|
||||
|
||||
// Iterate through all blocks updating their in/outs
|
||||
for (int i = index; i < blocks_.size(); i++) {
|
||||
Block *b = blocks_.at(i);
|
||||
|
||||
b->set_in(last_out);
|
||||
|
||||
last_out += b->length();
|
||||
|
||||
b->set_out(last_out);
|
||||
}
|
||||
|
||||
emit blocks_refreshed();
|
||||
|
||||
// Update track length
|
||||
emit track_length_changed();
|
||||
}
|
||||
|
||||
int Track::get_array_index_from_block(Block *block) const
|
||||
{
|
||||
return block_array_indexes_.at(blocks_.indexOf(block));
|
||||
}
|
||||
|
||||
int Track::get_array_index_from_cache_index(int index) const
|
||||
{
|
||||
return block_array_indexes_.at(index);
|
||||
}
|
||||
|
||||
int Track::get_cache_index_from_array_index(int index) const
|
||||
{
|
||||
return block_array_indexes_.indexOf(index);
|
||||
}
|
||||
|
||||
int Track::get_block_index_at_time(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::process_audio_track(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[k_block_input].to_array();
|
||||
|
||||
for (auto it = arr.cbegin(); it != arr.cend(); it++) {
|
||||
Block *b = blocks_.at(get_cache_index_from_array_index(it->first));
|
||||
|
||||
TimeRange range_for_block(qMax(b->in(), range.in()),
|
||||
qMin(b->out(), range.out()));
|
||||
|
||||
qint64 source_offset = 0;
|
||||
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.to_samples();
|
||||
|
||||
if (samples_from_this_block.is_allocated()) {
|
||||
// If this is a clip, we might have extra speed/reverse information
|
||||
if (ClipBlock *clip_cast = dynamic_cast<ClipBlock *>(b)) {
|
||||
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; i < out.size(); i++) {
|
||||
memcpy(new_samples.data(i),
|
||||
out[i].data(),
|
||||
out[i].size());
|
||||
}
|
||||
|
||||
samples_from_this_block = new_samples;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
} else {
|
||||
// Multiply time
|
||||
samples_from_this_block.speed(speed_value);
|
||||
}
|
||||
}
|
||||
|
||||
if (reversed) {
|
||||
samples_from_this_block.reverse();
|
||||
}
|
||||
}
|
||||
|
||||
qint64 copy_length = qMin(
|
||||
max_dest_sz,
|
||||
qint64(samples_from_this_block.sample_count() - source_offset));
|
||||
|
||||
// Copy samples into destination buffer
|
||||
for (int i = 0;
|
||||
i < samples_from_this_block.audio_params().channel_count();
|
||||
i++) {
|
||||
block_range_buffer.set(
|
||||
i, samples_from_this_block.data(i) + source_offset,
|
||||
destination_offset, copy_length);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
table->push(NodeValue::k_samples, QVariant::fromValue(block_range_buffer),
|
||||
this);
|
||||
}
|
||||
|
||||
int Track::connect_block(Block *b)
|
||||
{
|
||||
if (!empty_inputs_.empty()) {
|
||||
int index = empty_inputs_.front();
|
||||
empty_inputs_.pop_front();
|
||||
|
||||
Node::connect_edge(b, NodeInput(this, k_block_input, index));
|
||||
|
||||
return index;
|
||||
} else {
|
||||
int old_sz = input_array_size(k_block_input);
|
||||
input_array_append(k_block_input);
|
||||
Node::connect_edge(b, NodeInput(this, k_block_input, old_sz));
|
||||
return old_sz;
|
||||
}
|
||||
}
|
||||
|
||||
void Track::update_array_map()
|
||||
{
|
||||
ignore_arraymap_++;
|
||||
set_standard_value(
|
||||
k_array_map_input,
|
||||
QByteArray(reinterpret_cast<const char *>(block_array_indexes_.data()),
|
||||
block_array_indexes_.size() * sizeof(uint32_t)));
|
||||
}
|
||||
|
||||
void Track::refresh_block_cache_from_array_map()
|
||||
{
|
||||
if (ignore_arraymap_set_) {
|
||||
return;
|
||||
}
|
||||
|
||||
// Disconnecting any existing blocks
|
||||
for (Block *b : blocks_) {
|
||||
Q_ASSERT(b->track() == this);
|
||||
b->set_track(nullptr);
|
||||
b->set_previous(nullptr);
|
||||
b->set_next(nullptr);
|
||||
b->set_in(0);
|
||||
b->set_out(b->length());
|
||||
disconnect(b, &Block::length_changed, this, &Track::block_length_changed);
|
||||
}
|
||||
|
||||
QByteArray bytes = get_standard_value(k_array_map_input).toByteArray();
|
||||
block_array_indexes_.resize(bytes.size() / sizeof(uint32_t));
|
||||
memcpy(block_array_indexes_.data(), bytes.data(), bytes.size());
|
||||
blocks_.clear();
|
||||
blocks_.reserve(block_array_indexes_.size());
|
||||
|
||||
Block *prev = nullptr;
|
||||
arraymap_invalid_ = false;
|
||||
|
||||
for (int i = 0; i < block_array_indexes_.size(); i++) {
|
||||
Block *b = static_cast<Block *>(
|
||||
get_connected_output(k_block_input, block_array_indexes_.at(i)));
|
||||
|
||||
Block::set_previous_next(prev, b);
|
||||
|
||||
if (b) {
|
||||
b->set_track(this);
|
||||
connect(b, &Block::length_changed, this, &Track::block_length_changed);
|
||||
|
||||
blocks_.append(b);
|
||||
prev = b;
|
||||
} else {
|
||||
block_array_indexes_.resize(i);
|
||||
arraymap_invalid_ = true;
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
if (prev) {
|
||||
prev->set_next(nullptr);
|
||||
}
|
||||
|
||||
update_in_out_from(0);
|
||||
}
|
||||
|
||||
void Track::block_length_changed()
|
||||
{
|
||||
// Assumes sender is a Block
|
||||
Block *b = static_cast<Block *>(sender());
|
||||
|
||||
update_in_out_from(blocks_.indexOf(b));
|
||||
}
|
||||
|
||||
uint qHash(const Track::Reference &r, uint seed)
|
||||
{
|
||||
// Not super efficient, but couldn't think of any better way to ensure a different hash each time
|
||||
return ::qHash(QStringLiteral("%1:%2").arg(QString::number(r.type()),
|
||||
QString::number(r.index())),
|
||||
seed);
|
||||
}
|
||||
|
||||
QDataStream &operator<<(QDataStream &out, const Track::Reference &ref)
|
||||
{
|
||||
out << static_cast<int>(ref.type()) << ref.index();
|
||||
|
||||
return out;
|
||||
}
|
||||
|
||||
QDataStream &operator>>(QDataStream &in, Track::Reference &ref)
|
||||
{
|
||||
int type;
|
||||
int index;
|
||||
|
||||
in >> type >> index;
|
||||
|
||||
ref = Track::Reference(static_cast<Track::Type>(type), index);
|
||||
|
||||
return in;
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,518 @@
|
||||
/***
|
||||
|
||||
Olive - Non-Linear Video Editor
|
||||
Copyright (C) 2022 Olive Team
|
||||
Modifications Copyright (C) 2025 mikesolar
|
||||
|
||||
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 <http://www.gnu.org/licenses/>.
|
||||
|
||||
***/
|
||||
|
||||
#ifndef OAK_TRACK_H
|
||||
#define OAK_TRACK_H
|
||||
|
||||
#include "node/block/block.h"
|
||||
|
||||
namespace olive
|
||||
{
|
||||
|
||||
class Sequence;
|
||||
|
||||
/**
|
||||
* @brief A time traversal Node for sorting through one channel/track of Blocks
|
||||
*/
|
||||
class Track : public Node {
|
||||
Q_OBJECT
|
||||
public:
|
||||
enum Type { k_none = -1, k_video, k_audio, k_subtitle, k_count };
|
||||
|
||||
Track();
|
||||
|
||||
NODE_DEFAULT_FUNCTIONS(Track)
|
||||
|
||||
const Track::Type &type() const;
|
||||
void set_type(const Track::Type &track_type);
|
||||
|
||||
virtual QString name() const override;
|
||||
virtual QString id() const override;
|
||||
virtual QVector<CategoryID> category() const override;
|
||||
virtual QString description() const override;
|
||||
|
||||
virtual ActiveElements
|
||||
get_active_elements_at_time(const QString &input,
|
||||
const TimeRange &r) const override;
|
||||
virtual void value(const NodeValueRow &value, const NodeGlobals &globals,
|
||||
NodeValueTable *table) const override;
|
||||
|
||||
virtual TimeRange input_time_adjustment(const QString &input, int element,
|
||||
const TimeRange &input_time,
|
||||
bool clamp) const override;
|
||||
|
||||
virtual TimeRange
|
||||
output_time_adjustment(const QString &input, int element,
|
||||
const TimeRange &input_time) const override;
|
||||
|
||||
static Rational transform_time_for_block(const Block *block,
|
||||
const Rational &time)
|
||||
{
|
||||
if (time == RATIONAL_MAX || time == RATIONAL_MIN) {
|
||||
return time;
|
||||
}
|
||||
|
||||
return time - block->in();
|
||||
}
|
||||
|
||||
static TimeRange transform_range_for_block(const Block *block,
|
||||
const TimeRange &range)
|
||||
{
|
||||
return TimeRange(transform_time_for_block(block, range.in()),
|
||||
transform_time_for_block(block, range.out()));
|
||||
}
|
||||
|
||||
static Rational transform_time_from_block(const Block *block,
|
||||
const Rational &time)
|
||||
{
|
||||
if (time == RATIONAL_MAX || time == RATIONAL_MIN) {
|
||||
return time;
|
||||
}
|
||||
|
||||
return time + block->in();
|
||||
}
|
||||
|
||||
static TimeRange transform_range_from_block(const Block *block,
|
||||
const TimeRange &range)
|
||||
{
|
||||
return TimeRange(transform_time_from_block(block, range.in()),
|
||||
transform_time_from_block(block, range.out()));
|
||||
}
|
||||
|
||||
const double &get_track_height() const;
|
||||
void set_track_height(const double &height);
|
||||
|
||||
int get_track_height_in_pixels() const
|
||||
{
|
||||
return internal_height_to_pixel_height(get_track_height());
|
||||
}
|
||||
|
||||
void set_track_height_in_pixels(int h)
|
||||
{
|
||||
set_track_height(pixel_height_to_internal_height(h));
|
||||
}
|
||||
|
||||
virtual bool load_custom(QXmlStreamReader *reader,
|
||||
SerializedData *data) override;
|
||||
virtual void save_custom(QXmlStreamWriter *writer) const override;
|
||||
virtual void PostLoadEvent(SerializedData *data) override;
|
||||
|
||||
static int internal_height_to_pixel_height(double h)
|
||||
{
|
||||
return qRound(h * QFontMetrics(QFont()).height());
|
||||
}
|
||||
|
||||
static double pixel_height_to_internal_height(int h)
|
||||
{
|
||||
return double(h) / double(QFontMetrics(QFont()).height());
|
||||
}
|
||||
|
||||
static int get_default_track_height_in_pixels()
|
||||
{
|
||||
return internal_height_to_pixel_height(k_track_height_default);
|
||||
}
|
||||
|
||||
static int get_minimum_track_height_in_pixels()
|
||||
{
|
||||
return internal_height_to_pixel_height(k_track_height_minimum);
|
||||
}
|
||||
|
||||
virtual void retranslate() override;
|
||||
|
||||
class Reference {
|
||||
public:
|
||||
Reference()
|
||||
: type_(k_none)
|
||||
, index_(-1)
|
||||
{
|
||||
}
|
||||
|
||||
Reference(const Track::Type &type, const int &index)
|
||||
: type_(type)
|
||||
, index_(index)
|
||||
{
|
||||
}
|
||||
|
||||
const Track::Type &type() const
|
||||
{
|
||||
return type_;
|
||||
}
|
||||
|
||||
const int &index() const
|
||||
{
|
||||
return index_;
|
||||
}
|
||||
|
||||
bool operator==(const Reference &ref) const
|
||||
{
|
||||
return type_ == ref.type_ && index_ == ref.index_;
|
||||
}
|
||||
|
||||
bool operator!=(const Reference &ref) const
|
||||
{
|
||||
return !(*this == ref);
|
||||
}
|
||||
|
||||
bool operator<(const Track::Reference &rhs) const
|
||||
{
|
||||
if (type_ != rhs.type_) {
|
||||
return type_ < rhs.type_;
|
||||
}
|
||||
|
||||
return index_ < rhs.index_;
|
||||
}
|
||||
|
||||
QString to_string() const
|
||||
{
|
||||
QString type_string = type_to_string(type_);
|
||||
if (type_string.isEmpty()) {
|
||||
return QString();
|
||||
} else {
|
||||
return QStringLiteral("%1:%2").arg(type_string,
|
||||
QString::number(index_));
|
||||
}
|
||||
}
|
||||
|
||||
/// For IDs that shouldn't change between localizations
|
||||
static QString type_to_string(Type type)
|
||||
{
|
||||
switch (type) {
|
||||
case k_video:
|
||||
return QStringLiteral("v");
|
||||
case k_audio:
|
||||
return QStringLiteral("a");
|
||||
case k_subtitle:
|
||||
return QStringLiteral("s");
|
||||
case k_count:
|
||||
case k_none:
|
||||
break;
|
||||
}
|
||||
|
||||
return QString();
|
||||
}
|
||||
|
||||
/// For human-facing strings
|
||||
static QString type_to_translated_string(Type type)
|
||||
{
|
||||
switch (type) {
|
||||
case k_video:
|
||||
return tr("V");
|
||||
case k_audio:
|
||||
return tr("A");
|
||||
case k_subtitle:
|
||||
return tr("S");
|
||||
case k_count:
|
||||
case k_none:
|
||||
break;
|
||||
}
|
||||
|
||||
return QString();
|
||||
}
|
||||
|
||||
static Type type_from_string(const QString &s)
|
||||
{
|
||||
if (s.size() >= 3) {
|
||||
if (s.at(1) == ':') {
|
||||
if (s.at(0) == 'v') {
|
||||
// Video stream
|
||||
return Track::k_video;
|
||||
} else if (s.at(0) == 'a') {
|
||||
// Audio stream
|
||||
return Track::k_audio;
|
||||
} else if (s.at(0) == 's') {
|
||||
// Subtitle stream
|
||||
return Track::k_subtitle;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return Track::k_none;
|
||||
}
|
||||
|
||||
static Reference from_string(const QString &s)
|
||||
{
|
||||
Reference ref;
|
||||
Type parse_type = type_from_string(s);
|
||||
|
||||
if (parse_type != Track::k_none) {
|
||||
bool ok;
|
||||
int parse_index = s.mid(2).toInt(&ok);
|
||||
|
||||
if (ok) {
|
||||
ref.type_ = parse_type;
|
||||
ref.index_ = parse_index;
|
||||
}
|
||||
}
|
||||
|
||||
return ref;
|
||||
}
|
||||
|
||||
bool is_valid() const
|
||||
{
|
||||
return type_ > k_none && type_ < k_count && index_ >= 0;
|
||||
}
|
||||
|
||||
private:
|
||||
Track::Type type_;
|
||||
|
||||
int index_;
|
||||
};
|
||||
|
||||
Reference to_reference() const
|
||||
{
|
||||
return Reference(type(), index());
|
||||
}
|
||||
|
||||
const int &index() const
|
||||
{
|
||||
return index_;
|
||||
}
|
||||
|
||||
void set_index(const int &index);
|
||||
|
||||
/**
|
||||
* @brief Returns the block that starts BEFORE (not AT) and ends AFTER (not AT) a time
|
||||
*
|
||||
* Catches the first block that matches `block.in < time && block.out > time` or nullptr if any
|
||||
* block starts/ends precisely at that time or the time exceeds the track length.
|
||||
*/
|
||||
Block *block_containing_time(const Rational &time) const;
|
||||
|
||||
/**
|
||||
* @brief Returns the block that starts BEFORE a given time and ends either AFTER or AT that time
|
||||
*
|
||||
* @return Catches the first block that matches `block.out >= time` or nullptr if this time
|
||||
* exceeds the track length.
|
||||
*/
|
||||
Block *nearest_block_before(const Rational &time) const;
|
||||
|
||||
/**
|
||||
* @brief Returns the block that starts BEFORE or AT a given time.
|
||||
*
|
||||
* @return Catches the first block that matches `block.out > time` or nullptr if this time
|
||||
* exceeds the track length.
|
||||
*/
|
||||
Block *nearest_block_before_or_at(const Rational &time) const;
|
||||
|
||||
/**
|
||||
* @brief Returns the block that starts either AT a given time or the soonest block AFTER
|
||||
*
|
||||
* @return Catches the first block that matches `block.in >= time` or nullptr if this time
|
||||
* exceeds the track length.
|
||||
*/
|
||||
Block *nearest_block_after_or_at(const Rational &time) const;
|
||||
|
||||
/**
|
||||
* @brief Returns the block that starts AFTER the given time (but never AT the given time)
|
||||
*
|
||||
* @return Catches the first block that matches `block.in > time` or nullptr if this time
|
||||
* exceeds the track length.
|
||||
*/
|
||||
Block *nearest_block_after(const Rational &time) const;
|
||||
|
||||
/*
|
||||
* @brief Returns whether a time range is empty or only has a gap
|
||||
*/
|
||||
bool is_range_free(const TimeRange &range) const;
|
||||
|
||||
const QVector<Block *> &blocks() const
|
||||
{
|
||||
return blocks_;
|
||||
}
|
||||
|
||||
virtual void invalidate_cache(const TimeRange &range, const QString &from,
|
||||
int element,
|
||||
InvalidateCacheOptions options) override;
|
||||
|
||||
Block *visible_block_at_time(const Rational &t) const
|
||||
{
|
||||
int index = get_block_index_at_time(t);
|
||||
return (index == -1) ? nullptr : blocks_.at(index);
|
||||
}
|
||||
|
||||
/**
|
||||
* @brief Adds Block `block` at the very beginning of the Sequence before all other clips
|
||||
*/
|
||||
void prepend_block(Block *block);
|
||||
|
||||
/**
|
||||
* @brief Inserts Block `block` at a specific index (0 is the start of the timeline)
|
||||
*
|
||||
* If the index == 0, this function does the same as PrependBlock(). If the index >= the current number of blocks,
|
||||
* this function is the same as AppendBlock().
|
||||
*/
|
||||
void insert_block_at_index(Block *block, int index);
|
||||
|
||||
/**
|
||||
* @brief Inserts Block after another Block
|
||||
*
|
||||
* Equivalent to calling InsertBlockBetweenBlocks(block, before, before->next())
|
||||
*/
|
||||
void insert_block_after(Block *block, Block *before);
|
||||
|
||||
/**
|
||||
* @brief Inserts Block before another Block
|
||||
*/
|
||||
void insert_block_before(Block *block, Block *after);
|
||||
|
||||
/**
|
||||
* @brief Adds Block `block` at the very end of the Sequence after all other clips
|
||||
*/
|
||||
void append_block(Block *block);
|
||||
|
||||
/**
|
||||
* @brief Removes a Block pushing all subsequent Blocks earlier to take up the space
|
||||
*/
|
||||
void ripple_remove_block(Block *block);
|
||||
|
||||
/**
|
||||
* @brief Replaces Block `old` with Block `replace`
|
||||
*
|
||||
* Both blocks must have equal lengths.
|
||||
*/
|
||||
void replace_block(Block *old, Block *replace);
|
||||
|
||||
Rational track_length() const;
|
||||
|
||||
bool is_muted() const;
|
||||
|
||||
bool is_locked() const;
|
||||
|
||||
int get_array_index_from_block(Block *block) const;
|
||||
|
||||
Sequence *sequence() const
|
||||
{
|
||||
return sequence_;
|
||||
}
|
||||
|
||||
void set_sequence(Sequence *sequence)
|
||||
{
|
||||
sequence_ = sequence;
|
||||
}
|
||||
|
||||
static const double k_track_height_default;
|
||||
static const double k_track_height_minimum;
|
||||
static const double k_track_height_interval;
|
||||
|
||||
static const QString k_block_input;
|
||||
static const QString k_muted_input;
|
||||
static const QString k_array_map_input;
|
||||
|
||||
public slots:
|
||||
void set_muted(bool e);
|
||||
|
||||
void set_locked(bool e);
|
||||
|
||||
signals:
|
||||
/**
|
||||
* @brief Signal emitted when a Block is added to this Track
|
||||
*/
|
||||
void block_added(Block *block);
|
||||
|
||||
/**
|
||||
* @brief Signal emitted when a Block is removed from this Track
|
||||
*/
|
||||
void block_removed(Block *block);
|
||||
|
||||
/**
|
||||
* @brief Signal emitted when the length of the track has changed
|
||||
*/
|
||||
void track_length_changed();
|
||||
|
||||
/**
|
||||
* @brief Signal emitted when the height of the track has changed
|
||||
*/
|
||||
void track_height_changed(qreal virtual_height);
|
||||
|
||||
/**
|
||||
* @brief Signal emitted when the muted setting changes
|
||||
*/
|
||||
void muted_changed(bool e);
|
||||
|
||||
/**
|
||||
* @brief Signal emitted when the index has changed
|
||||
*/
|
||||
void index_changed(int old, int now);
|
||||
|
||||
/**
|
||||
* @brief Emitted when a block changes length and all the subsequent blocks had to update
|
||||
*/
|
||||
void blocks_refreshed();
|
||||
|
||||
protected:
|
||||
virtual void InputConnectedEvent(const QString &input, int element,
|
||||
Node *node) override;
|
||||
virtual void InputValueChangedEvent(const QString &input,
|
||||
int element) override;
|
||||
|
||||
private:
|
||||
void update_in_out_from(int index);
|
||||
|
||||
int get_array_index_from_cache_index(int index) const;
|
||||
|
||||
int get_cache_index_from_array_index(int index) const;
|
||||
|
||||
int get_block_index_at_time(const Rational &time) const;
|
||||
|
||||
void process_audio_track(const NodeValueRow &value,
|
||||
const NodeGlobals &globals,
|
||||
NodeValueTable *table) const;
|
||||
|
||||
int connect_block(Block *b);
|
||||
|
||||
void update_array_map();
|
||||
|
||||
TimeRangeList block_length_pending_invalidations_;
|
||||
|
||||
QVector<Block *> blocks_;
|
||||
QVector<uint32_t> block_array_indexes_;
|
||||
|
||||
std::list<int> empty_inputs_;
|
||||
|
||||
Track::Type track_type_;
|
||||
|
||||
double track_height_;
|
||||
|
||||
int index_;
|
||||
|
||||
bool locked_;
|
||||
|
||||
Sequence *sequence_;
|
||||
|
||||
int ignore_arraymap_;
|
||||
bool arraymap_invalid_;
|
||||
bool ignore_arraymap_set_;
|
||||
|
||||
private slots:
|
||||
void block_length_changed();
|
||||
|
||||
void refresh_block_cache_from_array_map();
|
||||
};
|
||||
|
||||
uint qHash(const Track::Reference &r, uint seed = 0);
|
||||
|
||||
QDataStream &operator<<(QDataStream &out, const Track::Reference &ref);
|
||||
|
||||
QDataStream &operator>>(QDataStream &in, Track::Reference &ref);
|
||||
|
||||
}
|
||||
|
||||
#endif // OAK_TRACK_H
|
||||
@@ -0,0 +1,200 @@
|
||||
/***
|
||||
|
||||
Olive - Non-Linear Video Editor
|
||||
Copyright (C) 2022 Olive Team
|
||||
Modifications Copyright (C) 2025 mikesolar
|
||||
|
||||
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 <http://www.gnu.org/licenses/>.
|
||||
|
||||
***/
|
||||
|
||||
#include "tracklist.h"
|
||||
|
||||
#include "node/factory.h"
|
||||
#include "node/math/math/math.h"
|
||||
#include "node/math/merge/merge.h"
|
||||
#include "node/output/viewer/viewer.h"
|
||||
#include "node/project/sequence/sequence.h"
|
||||
|
||||
namespace olive
|
||||
{
|
||||
|
||||
TrackList::TrackList(Sequence *parent, const Track::Type &type,
|
||||
const QString &track_input)
|
||||
: QObject(parent)
|
||||
, track_input_(track_input)
|
||||
, total_length_(0)
|
||||
, type_(type)
|
||||
{
|
||||
}
|
||||
|
||||
Track *TrackList::get_track_at(int index) const
|
||||
{
|
||||
if (index >= 0 && index < track_cache_.size()) {
|
||||
return track_cache_.at(index);
|
||||
} else {
|
||||
return nullptr;
|
||||
}
|
||||
}
|
||||
|
||||
void TrackList::track_connected(Node *node, int element)
|
||||
{
|
||||
if (element == -1) {
|
||||
parent()->invalidate_all(track_input(), element);
|
||||
return;
|
||||
}
|
||||
|
||||
Track *track = dynamic_cast<Track *>(node);
|
||||
|
||||
if (!track) {
|
||||
return;
|
||||
}
|
||||
|
||||
// Determine where in the cache this block will be
|
||||
int cache_index = -1;
|
||||
for (int i = element + 1; i < array_size(); i++) {
|
||||
// Find next track because this will be the index we insert at
|
||||
cache_index = get_cache_index_from_array_index(i);
|
||||
|
||||
if (cache_index >= 0) {
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
// If there was no next, this will be inserted at the end
|
||||
if (cache_index == -1) {
|
||||
cache_index = track_cache_.size();
|
||||
}
|
||||
|
||||
track_cache_.insert(cache_index, track);
|
||||
track_array_indexes_.insert(cache_index, element);
|
||||
|
||||
// Update track indexes in the list (including this track)
|
||||
update_track_indexes_from(cache_index);
|
||||
|
||||
connect(track, &Track::track_length_changed, this,
|
||||
&TrackList::update_total_length);
|
||||
track_height_connections_.insert(
|
||||
track, connect(track, &Track::track_height_changed, this, [this]() {
|
||||
Track *t = static_cast<Track *>(sender());
|
||||
emit track_height_changed(t, t->get_track_height_in_pixels());
|
||||
}));
|
||||
|
||||
track->set_type(type_);
|
||||
track->set_sequence(parent());
|
||||
|
||||
emit track_list_changed();
|
||||
|
||||
// This function must be called after the track is added to track_cache_, since it uses track_cache_ to determine
|
||||
// the track's index
|
||||
emit track_added(track);
|
||||
|
||||
update_total_length();
|
||||
}
|
||||
|
||||
void TrackList::track_disconnected(Node *node, int element)
|
||||
{
|
||||
if (element == -1) {
|
||||
// User has replaced the entire array, we will invalidate everything
|
||||
parent()->invalidate_all(track_input(), element);
|
||||
return;
|
||||
}
|
||||
|
||||
Track *track = dynamic_cast<Track *>(node);
|
||||
|
||||
if (!track) {
|
||||
return;
|
||||
}
|
||||
|
||||
// Traverse through Tracks uncaching and disconnecting them
|
||||
emit track_removed(track);
|
||||
|
||||
int cache_index = get_cache_index_from_array_index(element);
|
||||
|
||||
// Remove track here
|
||||
track_cache_.removeAt(cache_index);
|
||||
track_array_indexes_.removeAt(cache_index);
|
||||
|
||||
// Update indices for all subsequent tracks
|
||||
update_track_indexes_from(cache_index);
|
||||
|
||||
track->set_index(-1);
|
||||
track->set_type(Track::k_none);
|
||||
track->set_sequence(nullptr);
|
||||
|
||||
disconnect(track, &Track::track_length_changed, this,
|
||||
&TrackList::update_total_length);
|
||||
disconnect(track_height_connections_.take(track));
|
||||
|
||||
emit track_list_changed();
|
||||
|
||||
update_total_length();
|
||||
}
|
||||
|
||||
void TrackList::update_track_indexes_from(int index)
|
||||
{
|
||||
for (int i = index; i < track_cache_.size(); i++) {
|
||||
track_cache_.at(i)->set_index(i);
|
||||
}
|
||||
}
|
||||
|
||||
Project *TrackList::get_parent_graph() const
|
||||
{
|
||||
return parent()->parent();
|
||||
}
|
||||
|
||||
const QString &TrackList::track_input() const
|
||||
{
|
||||
return track_input_;
|
||||
}
|
||||
|
||||
NodeInput TrackList::track_input(int element) const
|
||||
{
|
||||
return NodeInput(parent(), track_input(), element);
|
||||
}
|
||||
|
||||
Sequence *TrackList::parent() const
|
||||
{
|
||||
return static_cast<Sequence *>(QObject::parent());
|
||||
}
|
||||
|
||||
int TrackList::array_size() const
|
||||
{
|
||||
return parent()->input_array_size(track_input());
|
||||
}
|
||||
|
||||
void TrackList::array_append()
|
||||
{
|
||||
parent()->input_array_append(track_input());
|
||||
}
|
||||
|
||||
void TrackList::array_remove_last()
|
||||
{
|
||||
parent()->input_array_remove_last(track_input());
|
||||
}
|
||||
|
||||
void TrackList::update_total_length()
|
||||
{
|
||||
total_length_ = 0;
|
||||
|
||||
foreach (Track *track, track_cache_) {
|
||||
if (track) {
|
||||
total_length_ = qMax(total_length_, track->track_length());
|
||||
}
|
||||
}
|
||||
|
||||
emit length_changed(total_length_);
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,137 @@
|
||||
/***
|
||||
|
||||
Olive - Non-Linear Video Editor
|
||||
Copyright (C) 2022 Olive Team
|
||||
Modifications Copyright (C) 2025 mikesolar
|
||||
|
||||
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 <http://www.gnu.org/licenses/>.
|
||||
|
||||
***/
|
||||
|
||||
#ifndef OAK_TRACKLIST_H
|
||||
#define OAK_TRACKLIST_H
|
||||
|
||||
#include <QHash>
|
||||
#include <QObject>
|
||||
|
||||
#include "node/output/track/track.h"
|
||||
#include "timeline/timelinecommon.h"
|
||||
|
||||
namespace olive
|
||||
{
|
||||
|
||||
class Sequence;
|
||||
|
||||
class TrackList : public QObject {
|
||||
Q_OBJECT
|
||||
public:
|
||||
TrackList(Sequence *parent, const Track::Type &type,
|
||||
const QString &track_input);
|
||||
|
||||
const Track::Type &type() const
|
||||
{
|
||||
return type_;
|
||||
}
|
||||
|
||||
const QVector<Track *> &get_tracks() const
|
||||
{
|
||||
return track_cache_;
|
||||
}
|
||||
|
||||
Track *get_track_at(int index) const;
|
||||
|
||||
const Rational &get_total_length() const
|
||||
{
|
||||
return total_length_;
|
||||
}
|
||||
|
||||
int get_track_count() const
|
||||
{
|
||||
return track_cache_.size();
|
||||
}
|
||||
|
||||
Project *get_parent_graph() const;
|
||||
|
||||
const QString &track_input() const;
|
||||
NodeInput track_input(int element) const;
|
||||
|
||||
Sequence *parent() const;
|
||||
|
||||
int array_size() const;
|
||||
|
||||
void array_append();
|
||||
void array_remove_last();
|
||||
|
||||
int get_array_index_from_cache_index(int index) const
|
||||
{
|
||||
return track_array_indexes_.at(index);
|
||||
}
|
||||
|
||||
int get_cache_index_from_array_index(int index) const
|
||||
{
|
||||
return track_array_indexes_.indexOf(index);
|
||||
}
|
||||
|
||||
public slots:
|
||||
/**
|
||||
* @brief Slot for when the track connection is added
|
||||
*/
|
||||
void track_connected(Node *node, int element);
|
||||
|
||||
/**
|
||||
* @brief Slot for when the track connection is removed
|
||||
*/
|
||||
void track_disconnected(Node *node, int element);
|
||||
|
||||
signals:
|
||||
void track_list_changed();
|
||||
|
||||
void length_changed(const Rational &length);
|
||||
|
||||
void track_added(Track *track);
|
||||
|
||||
void track_removed(Track *track);
|
||||
|
||||
void track_height_changed(Track *track, int height);
|
||||
|
||||
private:
|
||||
void update_track_indexes_from(int index);
|
||||
|
||||
/**
|
||||
* @brief A cache of connected Tracks
|
||||
*/
|
||||
QVector<Track *> track_cache_;
|
||||
QVector<int> track_array_indexes_;
|
||||
|
||||
/**
|
||||
* @brief Stored TrackHeightChanged connections so they can be disconnected again
|
||||
*/
|
||||
QHash<Track *, QMetaObject::Connection> track_height_connections_;
|
||||
|
||||
QString track_input_;
|
||||
|
||||
Rational total_length_;
|
||||
|
||||
enum Track::Type type_;
|
||||
|
||||
private slots:
|
||||
/**
|
||||
* @brief Slot for when any of the track's length changes so we can update the length of the tracklist
|
||||
*/
|
||||
void update_total_length();
|
||||
};
|
||||
|
||||
}
|
||||
|
||||
#endif // OAK_TRACKLIST_H
|
||||
Reference in New Issue
Block a user