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:
2026-07-20 03:23:28 +08:00
parent 026ff94b5e
commit 28c4426236
604 changed files with 243 additions and 172 deletions
+23
View File
@@ -0,0 +1,23 @@
# 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/>.
add_subdirectory(track)
add_subdirectory(viewer)
set(OLIVE_SOURCES
${OLIVE_SOURCES}
PARENT_SCOPE
)
+24
View File
@@ -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
)
+881
View File
@@ -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;
}
}
+518
View File
@@ -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
+200
View File
@@ -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_);
}
}
+137
View File
@@ -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
+22
View File
@@ -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 <http://www.gnu.org/licenses/>.
set(OLIVE_SOURCES
${OLIVE_SOURCES}
node/output/viewer/viewer.h
node/output/viewer/viewer.cpp
PARENT_SCOPE
)
+668
View File
@@ -0,0 +1,668 @@
/***
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 "viewer.h"
#include "config/config.h"
#include "coreengine.h"
#include "node/traverser.h"
namespace olive
{
const QString ViewerOutput::k_video_params_input =
QStringLiteral("video_param_in");
const QString ViewerOutput::k_audio_params_input =
QStringLiteral("audio_param_in");
const QString ViewerOutput::k_subtitle_params_input =
QStringLiteral("subtitle_param_in");
const QString ViewerOutput::k_texture_input = QStringLiteral("tex_in");
const QString ViewerOutput::k_samples_input = QStringLiteral("samples_in");
const SampleFormat ViewerOutput::k_default_sample_format = SampleFormat::f32_p;
#define super Node
ViewerOutput::ViewerOutput(bool create_buffer_inputs,
bool create_default_streams)
: last_length_(0)
, video_length_(0)
, audio_length_(0)
, autocache_input_video_(true)
, autocache_input_audio_(false)
, waveform_requests_enabled_(false)
{
add_input(k_video_params_input, NodeValue::k_video_params,
InputFlags(k_input_flag_not_connectable | k_input_flag_not_keyframable |
k_input_flag_array | k_input_flag_hidden));
add_input(k_audio_params_input, NodeValue::k_audio_params,
InputFlags(k_input_flag_not_connectable | k_input_flag_not_keyframable |
k_input_flag_array | k_input_flag_hidden));
add_input(k_subtitle_params_input, NodeValue::k_subtitle_params,
InputFlags(k_input_flag_not_connectable | k_input_flag_not_keyframable |
k_input_flag_array | k_input_flag_hidden));
if (create_buffer_inputs) {
add_input(k_texture_input, NodeValue::k_texture,
InputFlags(k_input_flag_not_keyframable));
add_input(k_samples_input, NodeValue::k_samples,
InputFlags(k_input_flag_not_keyframable));
}
if (create_default_streams) {
add_stream(Track::k_video, QVariant());
add_stream(Track::k_audio, QVariant());
set_default_parameters();
}
set_flag(k_dont_show_in_param_view);
workarea_ = new TimelineWorkArea(this);
markers_ = new TimelineMarkerList(this);
}
QString ViewerOutput::name() const
{
return tr("Viewer");
}
QString ViewerOutput::id() const
{
return QStringLiteral("org.olivevideoeditor.Olive.vieweroutput");
}
QVector<Node::CategoryID> ViewerOutput::category() const
{
return { k_category_output };
}
QString ViewerOutput::description() const
{
return tr("Interface between a Viewer panel and the node system.");
}
QVariant ViewerOutput::data(const DataType &d) const
{
switch (d) {
case duration: {
Rational using_timebase;
Timecode::Display using_display =
EngineCore::instance()->get_timecode_display();
// Get first enabled streams
VideoParams video = get_first_enabled_video_stream();
AudioParams audio = get_first_enabled_audio_stream();
SubtitleParams sub = get_first_enabled_subtitle_stream();
if (video.is_valid() &&
video.video_type() != VideoParams::k_video_type_still) {
// Prioritize video
using_timebase = video.frame_rate_as_time_base();
} else if (audio.is_valid()) {
// Use audio as a backup
// If we're showing in a timecode, we prefer showing audio in seconds instead
if (using_display == Timecode::k_timecode_drop_frame ||
using_display == Timecode::k_timecode_non_drop_frame) {
using_display = Timecode::k_timecode_seconds;
}
using_timebase = audio.sample_rate_as_time_base();
} else if (sub.is_valid()) {
using_timebase =
OAK_CONFIG("DefaultSequenceFrameRate").value<Rational>();
}
if (!using_timebase.isNull()) {
// Return time transformed to timecode
return QString::fromStdString(Timecode::time_to_timecode(
get_length(), using_timebase, using_display));
}
break;
}
case frequency_rate: {
VideoParams video_stream;
if (has_enabled_video_streams() &&
(video_stream = get_first_enabled_video_stream()).video_type() !=
VideoParams::k_video_type_still) {
// This is a video editor, prioritize video streams
return tr("%1 FPS").arg(video_stream.frame_rate().to_double());
} else if (has_enabled_audio_streams()) {
// No video streams, return audio
AudioParams audio_stream = get_first_enabled_audio_stream();
return tr("%1 Hz").arg(audio_stream.sample_rate());
}
break;
}
default:
break;
}
return super::data(d);
}
bool ViewerOutput::has_enabled_video_streams() const
{
return get_first_enabled_video_stream().is_valid();
}
bool ViewerOutput::has_enabled_audio_streams() const
{
return get_first_enabled_audio_stream().is_valid();
}
bool ViewerOutput::has_enabled_subtitle_streams() const
{
return get_first_enabled_subtitle_stream().is_valid();
}
VideoParams ViewerOutput::get_first_enabled_video_stream() const
{
int sz = get_video_stream_count();
for (int i = 0; i < sz; i++) {
VideoParams vp = get_video_params(i);
if (vp.enabled()) {
return vp;
}
}
return VideoParams();
}
AudioParams ViewerOutput::get_first_enabled_audio_stream() const
{
int sz = get_audio_stream_count();
for (int i = 0; i < sz; i++) {
AudioParams ap = get_audio_params(i);
if (ap.enabled()) {
return ap;
}
}
return AudioParams();
}
SubtitleParams ViewerOutput::get_first_enabled_subtitle_stream() const
{
int sz = get_subtitle_stream_count();
for (int i = 0; i < sz; i++) {
SubtitleParams sp = get_subtitle_params(i);
if (sp.enabled()) {
return sp;
}
}
return SubtitleParams();
}
void ViewerOutput::set_default_parameters()
{
int width = OAK_CONFIG("DefaultSequenceWidth").toInt();
int height = OAK_CONFIG("DefaultSequenceHeight").toInt();
set_video_params(VideoParams(
width, height,
OAK_CONFIG("DefaultSequenceFrameRate").value<Rational>(),
static_cast<PixelFormat::Format>(
OAK_CONFIG("OfflinePixelFormat").toInt()),
VideoParams::k_internal_channel_count,
OAK_CONFIG("DefaultSequencePixelAspect").value<Rational>(),
OAK_CONFIG("DefaultSequenceInterlacing")
.value<VideoParams::Interlacing>(),
1));
set_audio_params(
AudioParams(OAK_CONFIG("DefaultSequenceAudioFrequency").toInt(),
OAK_CONFIG("DefaultSequenceAudioLayout").toULongLong(),
k_default_sample_format));
}
void ViewerOutput::invalidate_cache(const TimeRange &range, const QString &from,
int element, InvalidateCacheOptions options)
{
Q_UNUSED(element)
if (Node *connected = get_connected_output(from, element)) {
if (from == k_texture_input) {
//connected->thumbnail_cache()->Request(range.Intersected(max_range), PlaybackCache::kPreviewsOnly);
if (autocache_input_video_) {
TimeRange max_range = input_time_adjustment(
from, element, TimeRange(0, get_video_length()), false);
connected->video_frame_cache()->request(
this, range.intersected(max_range));
}
} else if (from == k_samples_input) {
TimeRange max_range = input_time_adjustment(
from, element, TimeRange(0, get_audio_length()), false);
if (waveform_requests_enabled_) {
connected->waveform_cache()->request(
this, range.intersected(max_range));
}
if (autocache_input_audio_) {
connected->audio_playback_cache()->request(
this, range.intersected(max_range));
}
}
}
verify_length();
super::invalidate_cache(range, from, element, options);
}
QVector<Track::Reference> ViewerOutput::get_enabled_streams_as_references() const
{
QVector<Track::Reference> refs;
{
int vp_sz = get_video_stream_count();
for (int i = 0; i < vp_sz; i++) {
if (get_video_params(i).enabled()) {
refs.append(Track::Reference(Track::k_video, i));
}
}
}
{
int ap_sz = get_audio_stream_count();
for (int i = 0; i < ap_sz; i++) {
if (get_audio_params(i).enabled()) {
refs.append(Track::Reference(Track::k_audio, i));
}
}
}
{
int sp_sz = get_subtitle_stream_count();
for (int i = 0; i < sp_sz; i++) {
if (get_subtitle_params(i).enabled()) {
refs.append(Track::Reference(Track::k_subtitle, i));
}
}
}
return refs;
}
void ViewerOutput::retranslate()
{
super::retranslate();
set_input_name(k_video_params_input, tr("Video Parameters"));
set_input_name(k_audio_params_input, tr("Audio Parameters"));
set_input_name(k_subtitle_params_input, tr("Subtitle Parameters"));
if (has_input_with_id(k_texture_input)) {
set_input_name(k_texture_input, tr("Texture"));
}
if (has_input_with_id(k_samples_input)) {
set_input_name(k_samples_input, tr("Samples"));
}
}
void ViewerOutput::verify_length()
{
video_length_ = verify_length_internal(Track::k_video);
audio_length_ = verify_length_internal(Track::k_audio);
Rational subtitle_length = verify_length_internal(Track::k_subtitle);
Rational real_length =
qMax(subtitle_length, qMax(video_length_, audio_length_));
if (real_length != last_length_) {
last_length_ = real_length;
emit length_changed(last_length_);
}
}
void ViewerOutput::set_playhead(const Rational &t)
{
playhead_ = t;
emit playhead_changed(t);
}
void ViewerOutput::InputConnectedEvent(const QString &input, int element,
Node *output)
{
if (input == k_texture_input) {
emit texture_input_changed();
} else if (input == k_samples_input) {
connect(output->waveform_cache(), &AudioWaveformCache::validated, this,
&ViewerOutput::connected_waveform_changed);
}
super::InputConnectedEvent(input, element, output);
}
void ViewerOutput::InputDisconnectedEvent(const QString &input, int element,
Node *output)
{
if (input == k_texture_input) {
emit texture_input_changed();
} else if (input == k_samples_input) {
disconnect(output->waveform_cache(), &AudioWaveformCache::validated,
this, &ViewerOutput::connected_waveform_changed);
}
super::InputDisconnectedEvent(input, element, output);
}
Rational ViewerOutput::verify_length_internal(Track::Type type) const
{
NodeTraverser traverser;
switch (type) {
case Track::k_video:
if (is_input_connected(k_texture_input)) {
NodeValueTable t = traverser.generate_table(
get_connected_output(k_texture_input), TimeRange(0, 0));
Rational r = t.get(NodeValue::k_rational, QStringLiteral("length"))
.to_rational();
if (!r.isNaN()) {
return r;
}
}
break;
case Track::k_audio:
if (is_input_connected(k_samples_input)) {
NodeValueTable t = traverser.generate_table(
get_connected_output(k_samples_input), TimeRange(0, 0));
Rational r = t.get(NodeValue::k_rational, QStringLiteral("length"))
.to_rational();
if (!r.isNaN()) {
return r;
}
}
break;
case Track::k_none:
case Track::k_subtitle:
case Track::k_count:
break;
}
return 0;
}
Node *ViewerOutput::get_connected_texture_output()
{
return get_connected_output(k_texture_input);
}
Node::ValueHint ViewerOutput::get_connected_texture_value_hint()
{
return get_value_hint_for_input(k_texture_input);
}
Node *ViewerOutput::get_connected_sample_output()
{
return get_connected_output(k_samples_input);
}
Node::ValueHint ViewerOutput::get_connected_sample_value_hint()
{
return get_value_hint_for_input(k_samples_input);
}
void ViewerOutput::set_waveform_enabled(bool e)
{
if ((waveform_requests_enabled_ = e)) {
if (Node *connected = this->get_connected_sample_output()) {
TimeRange max_range = input_time_adjustment(
k_samples_input, -1, TimeRange(0, get_audio_length()), false);
TimeRangeList invalid =
connected->waveform_cache()->get_invalidated_ranges(max_range);
for (const TimeRange &r : invalid) {
connected->waveform_cache()->request(this, r);
}
}
}
}
void ViewerOutput::value(const NodeValueRow &value, const NodeGlobals &globals,
NodeValueTable *table) const
{
if (has_input_with_id(k_texture_input)) {
NodeValue repush = value[k_texture_input];
repush.set_tag(Track::Reference(Track::k_video, 0).to_string());
table->push(repush);
}
if (has_input_with_id(k_samples_input)) {
NodeValue repush = value[k_samples_input];
repush.set_tag(Track::Reference(Track::k_audio, 0).to_string());
table->push(repush);
}
}
bool ViewerOutput::load_custom(QXmlStreamReader *reader, SerializedData *data)
{
while (xml_read_next_start_element(reader)) {
if (reader->name() == QStringLiteral("markers")) {
if (!this->get_markers()->load(reader)) {
return false;
}
} else if (reader->name() == QStringLiteral("workarea")) {
if (!this->get_work_area()->load(reader)) {
return false;
}
} else {
reader->skipCurrentElement();
}
}
return true;
}
void ViewerOutput::save_custom(QXmlStreamWriter *writer) const
{
writer->writeStartElement(QStringLiteral("workarea"));
this->get_work_area()->save(writer);
writer->writeEndElement(); // workarea
writer->writeStartElement(QStringLiteral("markers"));
this->get_markers()->save(writer);
writer->writeEndElement(); // markers
}
void ViewerOutput::InputValueChangedEvent(const QString &input, int element)
{
if (element == 0) {
if (input == k_video_params_input) {
VideoParams new_video_params = get_video_params();
bool has_size_changed =
cached_video_params_.width() != new_video_params.width() ||
cached_video_params_.height() != new_video_params.height();
bool has_frame_rate_changed = cached_video_params_.frame_rate() !=
new_video_params.frame_rate();
bool has_pixel_aspect_changed =
cached_video_params_.pixel_aspect_ratio() !=
new_video_params.pixel_aspect_ratio();
bool has_interlacing_changed = cached_video_params_.interlacing() !=
new_video_params.interlacing();
if (has_size_changed) {
emit size_changed(new_video_params.width(),
new_video_params.height());
}
if (has_pixel_aspect_changed) {
emit pixel_aspect_changed(new_video_params.pixel_aspect_ratio());
}
if (has_interlacing_changed) {
emit interlacing_changed(new_video_params.interlacing());
}
if (has_frame_rate_changed) {
emit frame_rate_changed(new_video_params.frame_rate());
}
emit video_params_changed();
cached_video_params_ = new_video_params;
} else if (input == k_audio_params_input) {
AudioParams new_audio_params = get_audio_params();
bool has_sample_rate_changed = new_audio_params.sample_rate() !=
cached_audio_params_.sample_rate();
if (has_sample_rate_changed) {
emit sample_rate_changed(new_audio_params.sample_rate());
}
emit audio_params_changed();
cached_audio_params_ = new_audio_params;
}
}
super::InputValueChangedEvent(input, element);
}
void ViewerOutput::set_parameters_from_footage(
const QVector<ViewerOutput *> footage)
{
foreach (ViewerOutput *f, footage) {
QVector<VideoParams> video_streams = f->get_enabled_video_streams();
QVector<AudioParams> audio_streams = f->get_enabled_audio_streams();
for (int i = 0; i < video_streams.size(); i++) {
const VideoParams &s = video_streams.at(i);
bool found_video_params = false;
Rational using_timebase;
if (s.video_type() == VideoParams::k_video_type_still) {
// If this is a still image, we'll use it's resolution but won't set
// `found_video_params` in case something with a frame rate comes along which we'll
// prioritize
if (i > 0) {
// Ignore still images past stream 0
continue;
}
using_timebase = get_video_params().time_base();
} else {
using_timebase = s.frame_rate_as_time_base();
found_video_params = true;
}
set_video_params(
VideoParams(s.width(), s.height(), using_timebase,
static_cast<PixelFormat::Format>(
OAK_CONFIG("OfflinePixelFormat").toInt()),
VideoParams::k_internal_channel_count,
s.pixel_aspect_ratio(), s.interlacing(), 1));
if (found_video_params) {
break;
}
}
if (!audio_streams.isEmpty()) {
const AudioParams &s = audio_streams.first();
set_audio_params(AudioParams(s.sample_rate(), s.channel_layout(),
k_default_sample_format));
}
}
}
int ViewerOutput::add_stream(Track::Type type, const QVariant &value)
{
return set_stream(type, value, -1);
}
int ViewerOutput::set_stream(Track::Type type, const QVariant &value,
int index_in)
{
QString id;
if (type == Track::k_video) {
id = k_video_params_input;
} else if (type == Track::k_audio) {
id = k_audio_params_input;
} else if (type == Track::k_subtitle) {
id = k_subtitle_params_input;
} else {
return -1;
}
// Add another video/audio param to the array for this stream
int index = (index_in == -1) ? input_array_size(id) : index_in;
if (index >= input_array_size(id)) {
input_array_resize(id, index + 1);
}
set_standard_value(id, value, index);
return index;
}
QVector<VideoParams> ViewerOutput::get_enabled_video_streams() const
{
QVector<VideoParams> streams;
int vp_sz = get_video_stream_count();
for (int i = 0; i < vp_sz; i++) {
VideoParams vp = get_video_params(i);
if (vp.enabled()) {
streams.append(vp);
}
}
return streams;
}
QVector<AudioParams> ViewerOutput::get_enabled_audio_streams() const
{
QVector<AudioParams> streams;
int ap_sz = get_audio_stream_count();
for (int i = 0; i < ap_sz; i++) {
AudioParams ap = get_audio_params(i);
if (ap.enabled()) {
streams.append(ap);
}
}
return streams;
}
}
+314
View File
@@ -0,0 +1,314 @@
/***
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_VIEWER_H
#define OAK_VIEWER_H
#include "codec/encoder.h"
#include "node/node.h"
#include "node/output/track/track.h"
#include "render/audioplaybackcache.h"
#include "render/framehashcache.h"
#include "render/subtitleparams.h"
#include "render/videoparams.h"
#include "timeline/timelinemarker.h"
#include "timeline/timelineworkarea.h"
namespace olive
{
class Footage;
/**
* @brief A bridge between a node system and a ViewerPanel
*
* Receives update/time change signals from ViewerPanels and responds by sending them a texture of that frame
*/
class ViewerOutput : public Node {
Q_OBJECT
public:
ViewerOutput(bool create_buffer_inputs = true,
bool create_default_streams = true);
NODE_DEFAULT_FUNCTIONS(ViewerOutput)
virtual QString name() const override;
virtual QString id() const override;
virtual QVector<CategoryID> category() const override;
virtual QString description() const override;
virtual QVariant data(const DataType &d) const override;
void set_default_parameters();
void set_parameters_from_footage(const QVector<ViewerOutput *> footage);
virtual void invalidate_cache(const TimeRange &range, const QString &from,
int element,
InvalidateCacheOptions options) override;
VideoParams get_video_params(int index = 0) const
{
// This check isn't strictly necessary (GetStandardValue will return a null VideoParams anyway),
// but it does suppress a warning message that we don't need
if (index < input_array_size(k_video_params_input)) {
return get_standard_value(k_video_params_input, index)
.value<VideoParams>();
} else {
return VideoParams();
}
}
AudioParams get_audio_params(int index = 0) const
{
// This check isn't strictly necessary (GetStandardValue will return a null VideoParams anyway),
// but it does suppress a warning message that we don't need
if (index < input_array_size(k_audio_params_input)) {
return get_standard_value(k_audio_params_input, index)
.value<AudioParams>();
} else {
return AudioParams();
}
}
SubtitleParams get_subtitle_params(int index = 0) const
{
// This check isn't strictly necessary (GetStandardValue will return a null VideoParams anyway),
// but it does suppress a warning message that we don't need
if (index < input_array_size(k_subtitle_params_input)) {
return get_standard_value(k_subtitle_params_input, index)
.value<SubtitleParams>();
} else {
return SubtitleParams();
}
}
const Rational &get_playhead()
{
return playhead_;
}
void set_video_params(const VideoParams &video, int index = 0)
{
set_standard_value(k_video_params_input, QVariant::fromValue(video), index);
}
void set_audio_params(const AudioParams &audio, int index = 0)
{
set_standard_value(k_audio_params_input, QVariant::fromValue(audio), index);
}
void set_subtitle_params(const SubtitleParams &subs, int index = 0)
{
set_standard_value(k_subtitle_params_input, QVariant::fromValue(subs),
index);
}
int get_video_stream_count() const
{
return input_array_size(k_video_params_input);
}
int get_audio_stream_count() const
{
return input_array_size(k_audio_params_input);
}
int get_subtitle_stream_count() const
{
return input_array_size(k_subtitle_params_input);
}
virtual int get_total_stream_count() const
{
return get_video_stream_count() + get_audio_stream_count() +
get_subtitle_stream_count();
}
const AudioWaveformCache *get_connected_waveform()
{
if (Node *n = get_connected_sample_output()) {
return n->waveform_cache();
} else {
return nullptr;
}
}
bool has_enabled_video_streams() const;
bool has_enabled_audio_streams() const;
bool has_enabled_subtitle_streams() const;
VideoParams get_first_enabled_video_stream() const;
AudioParams get_first_enabled_audio_stream() const;
SubtitleParams get_first_enabled_subtitle_stream() const;
const Rational &get_length() const
{
return last_length_;
}
const Rational &get_video_length() const
{
return video_length_;
}
const Rational &get_audio_length() const
{
return audio_length_;
}
TimelineWorkArea *get_work_area() const
{
return workarea_;
}
TimelineMarkerList *get_markers() const
{
return markers_;
}
virtual TimeRange get_video_cache_range() const override
{
return TimeRange(0, get_video_length());
}
virtual TimeRange get_audio_cache_range() const override
{
return TimeRange(0, get_audio_length());
}
QVector<Track::Reference> get_enabled_streams_as_references() const;
QVector<VideoParams> get_enabled_video_streams() const;
QVector<AudioParams> get_enabled_audio_streams() const;
virtual void retranslate() override;
virtual Node *get_connected_texture_output();
virtual ValueHint get_connected_texture_value_hint();
virtual Node *get_connected_sample_output();
virtual ValueHint get_connected_sample_value_hint();
void set_waveform_enabled(bool e);
bool is_video_auto_cache_enabled() const
{
qDebug() << "sequence ac is a stub";
return false;
}
void set_video_auto_cache_enabled(bool e)
{
qDebug() << "sequence ac is a stub";
}
virtual void value(const NodeValueRow &value, const NodeGlobals &globals,
NodeValueTable *table) const override;
const EncodingParams &get_last_used_encoding_params() const
{
return last_used_encoding_params_;
}
void set_last_used_encoding_params(const EncodingParams &p)
{
last_used_encoding_params_ = p;
}
virtual bool load_custom(QXmlStreamReader *reader,
SerializedData *data) override;
virtual void save_custom(QXmlStreamWriter *writer) const override;
static const QString k_video_params_input;
static const QString k_audio_params_input;
static const QString k_subtitle_params_input;
static const QString k_texture_input;
static const QString k_samples_input;
static const SampleFormat k_default_sample_format;
signals:
void frame_rate_changed(const Rational &);
void length_changed(const Rational &length);
void size_changed(int width, int height);
void pixel_aspect_changed(const Rational &pixel_aspect);
void interlacing_changed(VideoParams::Interlacing mode);
void video_params_changed();
void audio_params_changed();
void texture_input_changed();
void sample_rate_changed(int sr);
void connected_waveform_changed();
void playhead_changed(const Rational &t);
public slots:
void verify_length();
void set_playhead(const Rational &t);
protected:
virtual void InputConnectedEvent(const QString &input, int element,
Node *output) override;
virtual void InputDisconnectedEvent(const QString &input, int element,
Node *output) override;
virtual Rational verify_length_internal(Track::Type type) const;
virtual void InputValueChangedEvent(const QString &input,
int element) override;
int add_stream(Track::Type type, const QVariant &value);
int set_stream(Track::Type type, const QVariant &value, int index);
private:
Rational last_length_;
Rational video_length_;
Rational audio_length_;
VideoParams cached_video_params_;
AudioParams cached_audio_params_;
TimelineWorkArea *workarea_;
TimelineMarkerList *markers_;
bool autocache_input_video_;
bool autocache_input_audio_;
EncodingParams last_used_encoding_params_;
bool waveform_requests_enabled_;
Rational playhead_;
};
}
#endif // OAK_VIEWER_H