- undoable track removal (content restored on undo) and true-move track reordering assembled from the edge commands (the engine has no move-track API) - track height/mute/lock getters and setters, documented as non-undoable to match the engine's current semantics - undoable marker add/remove/rename; duplicate timestamps are rejected with E_STATE instead of hitting the engine's debug assert
1068 lines
33 KiB
C++
1068 lines
33 KiB
C++
/***
|
|
|
|
Oak - Non-Linear Video Editor
|
|
Copyright (C) 2026 Oak 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/>.
|
|
|
|
***/
|
|
|
|
#include "oakengine/timeline.h"
|
|
|
|
#include <cstdio>
|
|
#include <cstring>
|
|
|
|
#include <QByteArray>
|
|
#include <QString>
|
|
|
|
#include "coreengine.h"
|
|
#include "node/block/clip/clip.h"
|
|
#include "node/nodeundo.h"
|
|
#include "node/project.h"
|
|
#include "node/project/folder/folder.h"
|
|
#include "node/project/sequence/sequence.h"
|
|
#include "timeline/timelinemarker.h"
|
|
#include "timeline/timelineundogeneral.h"
|
|
#include "timeline/timelineundopointer.h"
|
|
#include "timeline/timelineundoripple.h"
|
|
#include "timeline/timelineundosplit.h"
|
|
#include "timeline/timelineworkarea.h"
|
|
#include "undo/undocommand.h"
|
|
#include "undo/undostack.h"
|
|
|
|
// Internal cross-family accessor (not part of the public C ABI), defined in
|
|
// footage.cpp: borrowed project node of an import handle, nullptr otherwise.
|
|
extern "C" __attribute__((visibility("hidden"))) void *
|
|
oakengine_capi_footage_node(OakEngineFootage *h);
|
|
|
|
namespace
|
|
{
|
|
|
|
olive::Sequence *impl(OakEngineSequence *h)
|
|
{
|
|
return reinterpret_cast<olive::Sequence *>(h);
|
|
}
|
|
|
|
const olive::Sequence *impl(const OakEngineSequence *h)
|
|
{
|
|
return reinterpret_cast<const olive::Sequence *>(h);
|
|
}
|
|
|
|
OakEngineSequence *wrap(olive::Sequence *s)
|
|
{
|
|
return reinterpret_cast<OakEngineSequence *>(s);
|
|
}
|
|
|
|
// ViewerOutput::get_playhead() is not a const method in the engine; the
|
|
// facade keeps const-correct handles and casts locally.
|
|
olive::Sequence *mutable_impl(const OakEngineSequence *h)
|
|
{
|
|
return const_cast<olive::Sequence *>(
|
|
reinterpret_cast<const olive::Sequence *>(h));
|
|
}
|
|
|
|
// buf/size convention: returns the would-be length excluding the NUL.
|
|
int string_to_buf(const QString &s, char *buf, int buf_size)
|
|
{
|
|
const QByteArray utf = s.toUtf8();
|
|
if (buf && buf_size > 0) {
|
|
snprintf(buf, size_t(buf_size), "%s", utf.constData());
|
|
}
|
|
return int(utf.size());
|
|
}
|
|
|
|
// Copy a QString into a fixed-capacity C buffer, always NUL-terminating and
|
|
// truncating what does not fit.
|
|
void copy_to_buf(const QString &s, char *dst, size_t cap)
|
|
{
|
|
const QByteArray utf = s.toUtf8();
|
|
const size_t n = qMin(size_t(utf.size()), cap - 1);
|
|
memcpy(dst, utf.constData(), n);
|
|
dst[n] = '\0';
|
|
}
|
|
|
|
// The sequence's frame duration as a Rational timebase (frame rate flipped).
|
|
// Returns false when the sequence has no valid frame rate (no video params).
|
|
bool time_base_of(const olive::Sequence *s, olive::Rational *out)
|
|
{
|
|
const olive::Rational frame_rate = s->get_video_params().frame_rate();
|
|
if (frame_rate.isNull() || frame_rate.isNaN()) {
|
|
return false;
|
|
}
|
|
*out = frame_rate.flipped();
|
|
return true;
|
|
}
|
|
|
|
// Rational seconds -> timestamp in timebase units, like
|
|
// Timecode::time_to_timestamp with k_round rounding.
|
|
int64_t time_to_ts(const olive::Rational &time, const olive::Rational &tb)
|
|
{
|
|
return olive::core::Timecode::time_to_timestamp(
|
|
time, tb, olive::core::Timecode::k_round);
|
|
}
|
|
|
|
// ---- Editing primitive helpers -------------------------------------------
|
|
|
|
// Last editing error per thread (editing calls return NULL/negative codes).
|
|
thread_local QString g_seq_last_error;
|
|
|
|
void set_seq_error(const QString &error)
|
|
{
|
|
g_seq_last_error = error;
|
|
}
|
|
|
|
olive::Track::Type to_track_type(int track_type)
|
|
{
|
|
switch (track_type) {
|
|
case OAKENGINE_TRACK_TYPE_VIDEO:
|
|
return olive::Track::k_video;
|
|
case OAKENGINE_TRACK_TYPE_AUDIO:
|
|
return olive::Track::k_audio;
|
|
default:
|
|
return olive::Track::k_subtitle;
|
|
}
|
|
}
|
|
|
|
OakEngineClip *wrap_clip(olive::ClipBlock *c)
|
|
{
|
|
return reinterpret_cast<OakEngineClip *>(c);
|
|
}
|
|
|
|
// Push an undoable command onto the global undo stack when the engine is
|
|
// initialized, otherwise execute it directly (same degradation as the
|
|
// round-1 primitives).
|
|
void push_or_run(olive::UndoCommand *command, const QString &name)
|
|
{
|
|
if (olive::EngineCore::instance()) {
|
|
olive::EngineCore::instance()->undo_stack()->push(command, name);
|
|
} else {
|
|
command->redo_now();
|
|
delete command;
|
|
}
|
|
}
|
|
|
|
// The clip at (track_index, clip_index) within the given track list,
|
|
// skipping gap blocks; nullptr when out of range.
|
|
olive::ClipBlock *clip_at_index(olive::TrackList *list, int track_index,
|
|
int clip_index)
|
|
{
|
|
if (track_index < 0 || track_index >= list->get_track_count()) {
|
|
return nullptr;
|
|
}
|
|
int seen = 0;
|
|
for (olive::Block *b : list->get_track_at(track_index)->blocks()) {
|
|
if (olive::ClipBlock *clip = dynamic_cast<olive::ClipBlock *>(b)) {
|
|
if (seen == clip_index) {
|
|
return clip;
|
|
}
|
|
seen++;
|
|
}
|
|
}
|
|
return nullptr;
|
|
}
|
|
|
|
} // namespace
|
|
|
|
extern "C"
|
|
{
|
|
|
|
OakEngineSequence *oakengine_sequence_new(OakEngineProject *project,
|
|
const char *name)
|
|
{
|
|
olive::Project *p = reinterpret_cast<olive::Project *>(project);
|
|
if (!p || !p->root()) {
|
|
return nullptr;
|
|
}
|
|
|
|
olive::Sequence *sequence = new olive::Sequence();
|
|
sequence->set_default_parameters();
|
|
sequence->set_label(QString::fromUtf8(name ? name : ""));
|
|
|
|
// set_default_parameters() reads the sequence defaults from the user
|
|
// config; a config that lacks those keys yields invalid parameters
|
|
// (e.g. sample rate 0), which later aborts the render worker. Backfill
|
|
// hard defaults for anything invalid.
|
|
if (sequence->get_audio_params().sample_rate() <= 0) {
|
|
sequence->set_audio_params(
|
|
olive::AudioParams(48000, olive::core::k_channel_layout_stereo,
|
|
olive::core::SampleFormat::f32_p));
|
|
}
|
|
{
|
|
const olive::VideoParams vp = sequence->get_video_params();
|
|
const olive::Rational frame_rate = vp.frame_rate();
|
|
if (vp.width() <= 0 || vp.height() <= 0 || frame_rate.isNull() ||
|
|
frame_rate.isNaN()) {
|
|
const olive::PixelFormat::Format format =
|
|
vp.format() == olive::PixelFormat::invalid ?
|
|
olive::PixelFormat::f32 :
|
|
static_cast<olive::PixelFormat::Format>(vp.format());
|
|
sequence->set_video_params(olive::VideoParams(
|
|
1920, 1080, olive::Rational(1001, 30000), format,
|
|
olive::VideoParams::k_internal_channel_count));
|
|
}
|
|
}
|
|
|
|
// Same undoable creation as the application's "Create New Sequence"
|
|
// action (app/core.cpp), minus opening a viewer. Without an EngineCore
|
|
// (library not initialized) the command is executed non-undoably.
|
|
olive::MultiUndoCommand *command = new olive::MultiUndoCommand();
|
|
command->add_child(new olive::NodeAddCommand(p, sequence));
|
|
command->add_child(new olive::FolderAddChild(p->root(), sequence));
|
|
|
|
if (olive::EngineCore::instance()) {
|
|
olive::EngineCore::instance()->undo_stack()->push(
|
|
command, QStringLiteral("Create Sequence"));
|
|
} else {
|
|
command->redo_now();
|
|
delete command;
|
|
}
|
|
|
|
return wrap(sequence);
|
|
}
|
|
|
|
int oakengine_sequence_name(const OakEngineSequence *self, char *buf,
|
|
int buf_size)
|
|
{
|
|
if (!self) {
|
|
return OAKENGINE_E_INVALID;
|
|
}
|
|
return string_to_buf(impl(self)->get_label(), buf, buf_size);
|
|
}
|
|
|
|
int oakengine_sequence_get_length(const OakEngineSequence *self,
|
|
double *seconds)
|
|
{
|
|
if (!self || !seconds) {
|
|
return OAKENGINE_E_INVALID;
|
|
}
|
|
*seconds = impl(self)->get_length().to_double();
|
|
return OAKENGINE_OK;
|
|
}
|
|
|
|
int oakengine_sequence_get_length_rational(const OakEngineSequence *self,
|
|
int *num, int *den)
|
|
{
|
|
if (!self) {
|
|
return OAKENGINE_E_INVALID;
|
|
}
|
|
const olive::Rational length = impl(self)->get_length();
|
|
if (num) {
|
|
*num = length.numerator();
|
|
}
|
|
if (den) {
|
|
*den = length.denominator();
|
|
}
|
|
return OAKENGINE_OK;
|
|
}
|
|
|
|
int oakengine_sequence_get_frame_rate(const OakEngineSequence *self, int *num,
|
|
int *den)
|
|
{
|
|
if (!self) {
|
|
return OAKENGINE_E_INVALID;
|
|
}
|
|
const olive::Rational frame_rate = impl(self)->get_video_params().frame_rate();
|
|
if (frame_rate.isNull() || frame_rate.isNaN()) {
|
|
return OAKENGINE_E_STATE;
|
|
}
|
|
if (num) {
|
|
*num = frame_rate.numerator();
|
|
}
|
|
if (den) {
|
|
*den = frame_rate.denominator();
|
|
}
|
|
return OAKENGINE_OK;
|
|
}
|
|
|
|
int oakengine_sequence_get_video_params(const OakEngineSequence *self,
|
|
int *width, int *height, int *par_num,
|
|
int *par_den)
|
|
{
|
|
if (!self) {
|
|
return OAKENGINE_E_INVALID;
|
|
}
|
|
const olive::VideoParams params = impl(self)->get_video_params();
|
|
if (width) {
|
|
*width = params.width();
|
|
}
|
|
if (height) {
|
|
*height = params.height();
|
|
}
|
|
const olive::Rational par = params.pixel_aspect_ratio();
|
|
if (par_num) {
|
|
*par_num = par.numerator();
|
|
}
|
|
if (par_den) {
|
|
*par_den = par.denominator();
|
|
}
|
|
return OAKENGINE_OK;
|
|
}
|
|
|
|
int oakengine_sequence_track_count(const OakEngineSequence *self, int *video,
|
|
int *audio, int *subtitle)
|
|
{
|
|
if (!self) {
|
|
return OAKENGINE_E_INVALID;
|
|
}
|
|
const olive::Sequence *s = impl(self);
|
|
if (video) {
|
|
*video = s->track_list(olive::Track::k_video)->get_track_count();
|
|
}
|
|
if (audio) {
|
|
*audio = s->track_list(olive::Track::k_audio)->get_track_count();
|
|
}
|
|
if (subtitle) {
|
|
*subtitle = s->track_list(olive::Track::k_subtitle)->get_track_count();
|
|
}
|
|
return OAKENGINE_OK;
|
|
}
|
|
|
|
int oakengine_sequence_get_playhead(const OakEngineSequence *self,
|
|
int64_t *timestamp)
|
|
{
|
|
if (!self || !timestamp) {
|
|
return OAKENGINE_E_INVALID;
|
|
}
|
|
olive::Rational tb;
|
|
if (!time_base_of(impl(self), &tb)) {
|
|
return OAKENGINE_E_STATE;
|
|
}
|
|
*timestamp = time_to_ts(mutable_impl(self)->get_playhead(), tb);
|
|
return OAKENGINE_OK;
|
|
}
|
|
|
|
int oakengine_sequence_set_playhead(OakEngineSequence *self, int64_t timestamp)
|
|
{
|
|
if (!self) {
|
|
return OAKENGINE_E_INVALID;
|
|
}
|
|
olive::Rational tb;
|
|
if (!time_base_of(impl(self), &tb)) {
|
|
return OAKENGINE_E_STATE;
|
|
}
|
|
impl(self)->set_playhead(
|
|
olive::core::Timecode::timestamp_to_time(timestamp, tb));
|
|
return OAKENGINE_OK;
|
|
}
|
|
|
|
int oakengine_sequence_get_playhead_seconds(const OakEngineSequence *self,
|
|
double *seconds)
|
|
{
|
|
if (!self || !seconds) {
|
|
return OAKENGINE_E_INVALID;
|
|
}
|
|
*seconds = mutable_impl(self)->get_playhead().to_double();
|
|
return OAKENGINE_OK;
|
|
}
|
|
|
|
int oakengine_sequence_workarea_is_enabled(const OakEngineSequence *self)
|
|
{
|
|
return self && impl(self)->get_work_area()->enabled() ? 1 : 0;
|
|
}
|
|
|
|
int oakengine_sequence_get_workarea(const OakEngineSequence *self, int64_t *in,
|
|
int64_t *out)
|
|
{
|
|
if (!self) {
|
|
return OAKENGINE_E_INVALID;
|
|
}
|
|
olive::Rational tb;
|
|
if (!time_base_of(impl(self), &tb)) {
|
|
return OAKENGINE_E_STATE;
|
|
}
|
|
const olive::TimelineWorkArea *workarea = impl(self)->get_work_area();
|
|
if (in) {
|
|
*in = time_to_ts(workarea->in(), tb);
|
|
}
|
|
if (out) {
|
|
*out = time_to_ts(workarea->out(), tb);
|
|
}
|
|
return OAKENGINE_OK;
|
|
}
|
|
|
|
int oakengine_sequence_set_workarea(OakEngineSequence *self, int enabled,
|
|
int64_t in, int64_t out)
|
|
{
|
|
if (!self) {
|
|
return OAKENGINE_E_INVALID;
|
|
}
|
|
olive::Rational tb;
|
|
if (!time_base_of(impl(self), &tb)) {
|
|
return OAKENGINE_E_STATE;
|
|
}
|
|
olive::TimelineWorkArea *workarea = impl(self)->get_work_area();
|
|
workarea->set_enabled(enabled != 0);
|
|
workarea->set_range(
|
|
olive::TimeRange(olive::core::Timecode::timestamp_to_time(in, tb),
|
|
olive::core::Timecode::timestamp_to_time(out, tb)));
|
|
return OAKENGINE_OK;
|
|
}
|
|
|
|
int oakengine_sequence_marker_count(const OakEngineSequence *self)
|
|
{
|
|
if (!self) {
|
|
return 0;
|
|
}
|
|
return int(impl(self)->get_markers()->size());
|
|
}
|
|
|
|
int oakengine_sequence_marker_at(const OakEngineSequence *self, int index,
|
|
int64_t *time, char *name, int name_size)
|
|
{
|
|
if (!self || index < 0) {
|
|
return OAKENGINE_E_INVALID;
|
|
}
|
|
const olive::TimelineMarkerList *markers = impl(self)->get_markers();
|
|
if (size_t(index) >= markers->size()) {
|
|
return OAKENGINE_E_NOT_FOUND;
|
|
}
|
|
const olive::TimelineMarker *marker = *(markers->cbegin() + index);
|
|
if (time) {
|
|
olive::Rational tb;
|
|
if (!time_base_of(impl(self), &tb)) {
|
|
return OAKENGINE_E_STATE;
|
|
}
|
|
*time = time_to_ts(marker->time().in(), tb);
|
|
}
|
|
if (name && name_size > 0) {
|
|
copy_to_buf(marker->name(), name, size_t(name_size));
|
|
}
|
|
return OAKENGINE_OK;
|
|
}
|
|
|
|
/* ---- Timeline editing primitives ---------------------------------------- */
|
|
|
|
int oakengine_sequence_last_error(char *buf, int buf_size)
|
|
{
|
|
return string_to_buf(g_seq_last_error, buf, buf_size);
|
|
}
|
|
|
|
int oakengine_sequence_add_track(OakEngineSequence *self, int track_type)
|
|
{
|
|
set_seq_error(QString());
|
|
if (!self || track_type < OAKENGINE_TRACK_TYPE_VIDEO ||
|
|
track_type > OAKENGINE_TRACK_TYPE_SUBTITLE) {
|
|
set_seq_error(QStringLiteral("invalid sequence or track type"));
|
|
return OAKENGINE_E_INVALID;
|
|
}
|
|
|
|
olive::TrackList *list =
|
|
impl(self)->track_list(to_track_type(track_type));
|
|
// TimelineAddTrackCommand without auto-merge: the first video/audio
|
|
// track connects straight to the sequence output; further tracks stay
|
|
// unconnected (compositing is a later milestone).
|
|
auto *command = new olive::TimelineAddTrackCommand(list, false);
|
|
if (olive::EngineCore::instance()) {
|
|
olive::EngineCore::instance()->undo_stack()->push(
|
|
command, QStringLiteral("Add Track"));
|
|
} else {
|
|
command->redo_now();
|
|
delete command;
|
|
}
|
|
return list->get_track_count() - 1;
|
|
}
|
|
|
|
OakEngineClip *oakengine_sequence_add_footage_clip(
|
|
OakEngineSequence *seq, OakEngineFootage *footage, int track_type,
|
|
int track_index, int64_t in, int64_t out, int64_t media_in)
|
|
{
|
|
set_seq_error(QString());
|
|
olive::Sequence *sequence = reinterpret_cast<olive::Sequence *>(seq);
|
|
auto *footage_node =
|
|
static_cast<olive::Footage *>(oakengine_capi_footage_node(footage));
|
|
|
|
if (!sequence || !footage) {
|
|
set_seq_error(QStringLiteral("invalid sequence or footage handle"));
|
|
return nullptr;
|
|
}
|
|
if (!footage_node) {
|
|
set_seq_error(QStringLiteral(
|
|
"footage must be imported into the project first "
|
|
"(oakengine_project_import_footage)"));
|
|
return nullptr;
|
|
}
|
|
if (track_type != OAKENGINE_TRACK_TYPE_VIDEO &&
|
|
track_type != OAKENGINE_TRACK_TYPE_AUDIO) {
|
|
set_seq_error(QStringLiteral(
|
|
"clips are only supported on video and audio tracks"));
|
|
return nullptr;
|
|
}
|
|
olive::Project *project =
|
|
olive::Project::get_project_from_object(sequence);
|
|
if (!project ||
|
|
olive::Project::get_project_from_object(footage_node) != project) {
|
|
set_seq_error(QStringLiteral(
|
|
"footage and sequence belong to different projects"));
|
|
return nullptr;
|
|
}
|
|
if (in < 0 || out <= in || media_in < 0) {
|
|
set_seq_error(QStringLiteral("invalid clip range (need 0 <= in < out "
|
|
"and media_in >= 0)"));
|
|
return nullptr;
|
|
}
|
|
olive::TrackList *list = sequence->track_list(to_track_type(track_type));
|
|
if (track_index < 0 || track_index >= list->get_track_count()) {
|
|
set_seq_error(QStringLiteral("track index %1 out of range (%2 tracks)")
|
|
.arg(track_index)
|
|
.arg(list->get_track_count()));
|
|
return nullptr;
|
|
}
|
|
olive::Rational tb;
|
|
if (!time_base_of(sequence, &tb)) {
|
|
set_seq_error(QStringLiteral("sequence has no valid frame rate"));
|
|
return nullptr;
|
|
}
|
|
|
|
const olive::Rational in_time =
|
|
olive::core::Timecode::timestamp_to_time(in, tb);
|
|
const olive::Rational out_time =
|
|
olive::core::Timecode::timestamp_to_time(out, tb);
|
|
const olive::Rational media_in_time =
|
|
olive::core::Timecode::timestamp_to_time(media_in, tb);
|
|
|
|
// The application's drop-import chain reduced to its editing core (see
|
|
// ImportTool::place_at()): clip with media in-point and length, footage
|
|
// onto the buffer input, placed on the track -- all undoable.
|
|
auto *clip = new olive::ClipBlock();
|
|
clip->set_media_in(media_in_time);
|
|
clip->set_length_and_media_out(out_time - in_time);
|
|
|
|
olive::MultiUndoCommand *command = new olive::MultiUndoCommand();
|
|
command->add_child(new olive::NodeAddCommand(project, clip));
|
|
command->add_child(new olive::NodeEdgeAddCommand(
|
|
footage_node, olive::NodeInput(clip, olive::ClipBlock::k_buffer_in)));
|
|
command->add_child(new olive::TrackPlaceBlockCommand(list, track_index,
|
|
clip, in_time));
|
|
|
|
if (olive::EngineCore::instance()) {
|
|
olive::EngineCore::instance()->undo_stack()->push(
|
|
command, QStringLiteral("Add Clip"));
|
|
} else {
|
|
command->redo_now();
|
|
delete command;
|
|
}
|
|
return wrap_clip(clip);
|
|
}
|
|
|
|
int oakengine_sequence_clip_count(OakEngineSequence *self, int track_type,
|
|
int track_index)
|
|
{
|
|
if (!self || track_type < OAKENGINE_TRACK_TYPE_VIDEO ||
|
|
track_type > OAKENGINE_TRACK_TYPE_SUBTITLE) {
|
|
return OAKENGINE_E_INVALID;
|
|
}
|
|
olive::TrackList *list =
|
|
impl(self)->track_list(to_track_type(track_type));
|
|
if (track_index < 0 || track_index >= list->get_track_count()) {
|
|
return OAKENGINE_E_NOT_FOUND;
|
|
}
|
|
// Only real clips count; gap blocks on the track are skipped.
|
|
int count = 0;
|
|
for (const olive::Block *b : list->get_track_at(track_index)->blocks()) {
|
|
if (dynamic_cast<const olive::ClipBlock *>(b)) {
|
|
count++;
|
|
}
|
|
}
|
|
return count;
|
|
}
|
|
|
|
OakEngineClip *oakengine_sequence_clip_at(OakEngineSequence *self,
|
|
int track_type, int track_index,
|
|
int clip_index)
|
|
{
|
|
if (!self || track_type < OAKENGINE_TRACK_TYPE_VIDEO ||
|
|
track_type > OAKENGINE_TRACK_TYPE_SUBTITLE || clip_index < 0) {
|
|
return nullptr;
|
|
}
|
|
olive::TrackList *list =
|
|
impl(self)->track_list(to_track_type(track_type));
|
|
return wrap_clip(clip_at_index(list, track_index, clip_index));
|
|
}
|
|
|
|
int oakengine_clip_get_range(const OakEngineClip *self, int64_t *in,
|
|
int64_t *out, int64_t *media_in)
|
|
{
|
|
const olive::ClipBlock *clip =
|
|
reinterpret_cast<const olive::ClipBlock *>(self);
|
|
if (!clip) {
|
|
return OAKENGINE_E_INVALID;
|
|
}
|
|
// The clip's sequence (clip -> track -> sequence) provides the timebase
|
|
// for the timestamp conversion.
|
|
const olive::Sequence *sequence =
|
|
clip->track() ? clip->track()->sequence() : nullptr;
|
|
if (!sequence) {
|
|
return OAKENGINE_E_STATE;
|
|
}
|
|
olive::Rational tb;
|
|
if (!time_base_of(sequence, &tb)) {
|
|
return OAKENGINE_E_STATE;
|
|
}
|
|
if (in) {
|
|
*in = time_to_ts(clip->in(), tb);
|
|
}
|
|
if (out) {
|
|
*out = time_to_ts(clip->out(), tb);
|
|
}
|
|
if (media_in) {
|
|
*media_in = time_to_ts(clip->media_in(), tb);
|
|
}
|
|
return OAKENGINE_OK;
|
|
}
|
|
|
|
/* ---- Editing primitives, round 2 ----------------------------------------- */
|
|
|
|
int oakengine_sequence_split_clip(OakEngineSequence *seq, int track_type,
|
|
int track_index, int clip_index,
|
|
int64_t time)
|
|
{
|
|
set_seq_error(QString());
|
|
olive::Sequence *sequence = reinterpret_cast<olive::Sequence *>(seq);
|
|
if (!sequence || track_type < OAKENGINE_TRACK_TYPE_VIDEO ||
|
|
track_type > OAKENGINE_TRACK_TYPE_SUBTITLE) {
|
|
set_seq_error(QStringLiteral("invalid sequence or track type"));
|
|
return OAKENGINE_E_INVALID;
|
|
}
|
|
olive::TrackList *list =
|
|
sequence->track_list(to_track_type(track_type));
|
|
olive::ClipBlock *clip = clip_at_index(list, track_index, clip_index);
|
|
if (!clip) {
|
|
set_seq_error(QStringLiteral("no clip at track %1 index %2")
|
|
.arg(track_index)
|
|
.arg(clip_index));
|
|
return OAKENGINE_E_NOT_FOUND;
|
|
}
|
|
olive::Rational tb;
|
|
if (!time_base_of(sequence, &tb)) {
|
|
set_seq_error(QStringLiteral("sequence has no valid frame rate"));
|
|
return OAKENGINE_E_STATE;
|
|
}
|
|
const olive::Rational point =
|
|
olive::core::Timecode::timestamp_to_time(time, tb);
|
|
if (point <= clip->in() || point >= clip->out()) {
|
|
set_seq_error(QStringLiteral("split time %1 is not strictly inside "
|
|
"the clip [%2, %3]")
|
|
.arg(time)
|
|
.arg(time_to_ts(clip->in(), tb))
|
|
.arg(time_to_ts(clip->out(), tb)));
|
|
return OAKENGINE_E_INVALID;
|
|
}
|
|
|
|
push_or_run(new olive::BlockSplitCommand(clip, point),
|
|
QStringLiteral("Split Clip"));
|
|
return OAKENGINE_OK;
|
|
}
|
|
|
|
int oakengine_sequence_ripple_delete_clip(OakEngineSequence *seq,
|
|
int track_type, int track_index,
|
|
int clip_index)
|
|
{
|
|
set_seq_error(QString());
|
|
olive::Sequence *sequence = reinterpret_cast<olive::Sequence *>(seq);
|
|
if (!sequence || track_type < OAKENGINE_TRACK_TYPE_VIDEO ||
|
|
track_type > OAKENGINE_TRACK_TYPE_SUBTITLE) {
|
|
set_seq_error(QStringLiteral("invalid sequence or track type"));
|
|
return OAKENGINE_E_INVALID;
|
|
}
|
|
olive::TrackList *list =
|
|
sequence->track_list(to_track_type(track_type));
|
|
olive::ClipBlock *clip = clip_at_index(list, track_index, clip_index);
|
|
if (!clip || !clip->track()) {
|
|
set_seq_error(QStringLiteral("no clip at track %1 index %2")
|
|
.arg(track_index)
|
|
.arg(clip_index));
|
|
return OAKENGINE_E_NOT_FOUND;
|
|
}
|
|
|
|
push_or_run(new olive::TrackRippleRemoveAreaCommand(
|
|
clip->track(), olive::TimeRange(clip->in(), clip->out())),
|
|
QStringLiteral("Ripple Delete Clip"));
|
|
return OAKENGINE_OK;
|
|
}
|
|
|
|
int oakengine_clip_trim(OakEngineClip *clip, int64_t new_in, int64_t new_out)
|
|
{
|
|
set_seq_error(QString());
|
|
olive::ClipBlock *block = reinterpret_cast<olive::ClipBlock *>(clip);
|
|
if (!block) {
|
|
set_seq_error(QStringLiteral("invalid clip handle"));
|
|
return OAKENGINE_E_INVALID;
|
|
}
|
|
olive::Track *track = block->track();
|
|
if (!track) {
|
|
set_seq_error(QStringLiteral("clip is not on a track"));
|
|
return OAKENGINE_E_STATE;
|
|
}
|
|
const olive::Sequence *sequence = track->sequence();
|
|
olive::Rational tb;
|
|
if (!sequence || !time_base_of(sequence, &tb)) {
|
|
set_seq_error(QStringLiteral("sequence has no valid frame rate"));
|
|
return OAKENGINE_E_STATE;
|
|
}
|
|
if (new_in < 0 || new_out <= new_in) {
|
|
set_seq_error(QStringLiteral("invalid trim range (need 0 <= new_in "
|
|
"< new_out)"));
|
|
return OAKENGINE_E_INVALID;
|
|
}
|
|
|
|
const int64_t old_in = time_to_ts(block->in(), tb);
|
|
const int64_t old_out = time_to_ts(block->out(), tb);
|
|
if (new_in == old_in && new_out == old_out) {
|
|
return OAKENGINE_OK;
|
|
}
|
|
|
|
// The application's trim command (BlockTrimCommand): one end at a time,
|
|
// adjacent gaps absorb the difference; both ends are applied as an
|
|
// in-trim followed by an out-trim within one undoable command.
|
|
olive::MultiUndoCommand *command = new olive::MultiUndoCommand();
|
|
if (new_in != old_in) {
|
|
command->add_child(new olive::BlockTrimCommand(
|
|
track, block, block->out() -
|
|
olive::core::Timecode::timestamp_to_time(new_in, tb),
|
|
olive::Timeline::k_trim_in));
|
|
}
|
|
if (new_out != old_out) {
|
|
command->add_child(new olive::BlockTrimCommand(
|
|
track, block,
|
|
olive::core::Timecode::timestamp_to_time(new_out - new_in, tb),
|
|
olive::Timeline::k_trim_out));
|
|
}
|
|
push_or_run(command, QStringLiteral("Trim Clip"));
|
|
return OAKENGINE_OK;
|
|
}
|
|
|
|
int oakengine_sequence_move_clip(OakEngineSequence *seq, int track_type,
|
|
int track_index, int clip_index,
|
|
int64_t new_in)
|
|
{
|
|
set_seq_error(QString());
|
|
olive::Sequence *sequence = reinterpret_cast<olive::Sequence *>(seq);
|
|
if (!sequence || track_type < OAKENGINE_TRACK_TYPE_VIDEO ||
|
|
track_type > OAKENGINE_TRACK_TYPE_SUBTITLE || new_in < 0) {
|
|
set_seq_error(QStringLiteral("invalid arguments"));
|
|
return OAKENGINE_E_INVALID;
|
|
}
|
|
olive::TrackList *list =
|
|
sequence->track_list(to_track_type(track_type));
|
|
olive::ClipBlock *clip = clip_at_index(list, track_index, clip_index);
|
|
if (!clip || !clip->track()) {
|
|
set_seq_error(QStringLiteral("no clip at track %1 index %2")
|
|
.arg(track_index)
|
|
.arg(clip_index));
|
|
return OAKENGINE_E_NOT_FOUND;
|
|
}
|
|
olive::Rational tb;
|
|
if (!time_base_of(sequence, &tb)) {
|
|
set_seq_error(QStringLiteral("sequence has no valid frame rate"));
|
|
return OAKENGINE_E_STATE;
|
|
}
|
|
|
|
// Same assembly as the application's drag-move: gap the old spot, then
|
|
// place the clip at the destination (length and media in-point stay).
|
|
olive::MultiUndoCommand *command = new olive::MultiUndoCommand();
|
|
command->add_child(
|
|
new olive::TrackReplaceBlockWithGapCommand(clip->track(), clip));
|
|
command->add_child(new olive::TrackPlaceBlockCommand(
|
|
list, track_index, clip,
|
|
olive::core::Timecode::timestamp_to_time(new_in, tb)));
|
|
push_or_run(command, QStringLiteral("Move Clip"));
|
|
return OAKENGINE_OK;
|
|
}
|
|
|
|
/* ---- Track structure and markers ------------------------------------------ */
|
|
|
|
int oakengine_sequence_remove_track(OakEngineSequence *seq, int track_type,
|
|
int track_index)
|
|
{
|
|
set_seq_error(QString());
|
|
olive::Sequence *sequence = reinterpret_cast<olive::Sequence *>(seq);
|
|
if (!sequence || track_type < OAKENGINE_TRACK_TYPE_VIDEO ||
|
|
track_type > OAKENGINE_TRACK_TYPE_SUBTITLE) {
|
|
set_seq_error(QStringLiteral("invalid sequence or track type"));
|
|
return OAKENGINE_E_INVALID;
|
|
}
|
|
olive::TrackList *list =
|
|
sequence->track_list(to_track_type(track_type));
|
|
if (track_index < 0 || track_index >= list->get_track_count()) {
|
|
set_seq_error(QStringLiteral("no track at index %1")
|
|
.arg(track_index));
|
|
return OAKENGINE_E_NOT_FOUND;
|
|
}
|
|
push_or_run(new olive::TimelineRemoveTrackCommand(
|
|
list->get_track_at(track_index)),
|
|
QStringLiteral("Remove Track"));
|
|
return OAKENGINE_OK;
|
|
}
|
|
|
|
int oakengine_sequence_move_track(OakEngineSequence *seq, int track_type,
|
|
int from_index, int to_index)
|
|
{
|
|
set_seq_error(QString());
|
|
olive::Sequence *sequence = reinterpret_cast<olive::Sequence *>(seq);
|
|
if (!sequence || track_type < OAKENGINE_TRACK_TYPE_VIDEO ||
|
|
track_type > OAKENGINE_TRACK_TYPE_SUBTITLE) {
|
|
set_seq_error(QStringLiteral("invalid sequence or track type"));
|
|
return OAKENGINE_E_INVALID;
|
|
}
|
|
olive::TrackList *list =
|
|
sequence->track_list(to_track_type(track_type));
|
|
const int count = list->get_track_count();
|
|
if (from_index < 0 || from_index >= count || to_index < 0 ||
|
|
to_index >= count) {
|
|
set_seq_error(QStringLiteral("track index out of range (%1 tracks)")
|
|
.arg(count));
|
|
return OAKENGINE_E_NOT_FOUND;
|
|
}
|
|
if (from_index == to_index) {
|
|
return OAKENGINE_OK;
|
|
}
|
|
|
|
// True move: compute the reordered track list, then rewire every track's
|
|
// array-element connection into that order with the engine's edge
|
|
// commands (one undoable command, so undo restores the old order).
|
|
QVector<olive::Track *> order = list->get_tracks();
|
|
olive::Track *moved = order.at(from_index);
|
|
order.removeAt(from_index);
|
|
order.insert(to_index, moved);
|
|
|
|
// Array elements stay put (connections just move between them); collect
|
|
// them in slot order.
|
|
QVector<int> elements;
|
|
for (int i = 0; i < count; i++) {
|
|
elements.append(list->get_array_index_from_cache_index(i));
|
|
}
|
|
|
|
const QString input_id = list->track_input();
|
|
olive::MultiUndoCommand *command = new olive::MultiUndoCommand();
|
|
for (int i = 0; i < count; i++) {
|
|
command->add_child(new olive::NodeEdgeRemoveCommand(
|
|
list->get_tracks().at(i),
|
|
olive::NodeInput(sequence, input_id, elements.at(i))));
|
|
}
|
|
for (int i = 0; i < count; i++) {
|
|
command->add_child(new olive::NodeEdgeAddCommand(
|
|
order.at(i),
|
|
olive::NodeInput(sequence, input_id, elements.at(i))));
|
|
}
|
|
push_or_run(command, QStringLiteral("Move Track"));
|
|
return OAKENGINE_OK;
|
|
}
|
|
|
|
int oakengine_track_get_height(const OakEngineSequence *seq, int track_type,
|
|
int track_index, double *height)
|
|
{
|
|
set_seq_error(QString());
|
|
const olive::Sequence *sequence =
|
|
reinterpret_cast<const olive::Sequence *>(seq);
|
|
if (!sequence || !height || track_type < OAKENGINE_TRACK_TYPE_VIDEO ||
|
|
track_type > OAKENGINE_TRACK_TYPE_SUBTITLE) {
|
|
set_seq_error(QStringLiteral("invalid arguments"));
|
|
return OAKENGINE_E_INVALID;
|
|
}
|
|
olive::TrackList *list =
|
|
sequence->track_list(to_track_type(track_type));
|
|
if (track_index < 0 || track_index >= list->get_track_count()) {
|
|
set_seq_error(QStringLiteral("no track at index %1")
|
|
.arg(track_index));
|
|
return OAKENGINE_E_NOT_FOUND;
|
|
}
|
|
*height = list->get_track_at(track_index)->get_track_height();
|
|
return OAKENGINE_OK;
|
|
}
|
|
|
|
int oakengine_track_set_height(OakEngineSequence *seq, int track_type,
|
|
int track_index, double height)
|
|
{
|
|
set_seq_error(QString());
|
|
olive::Sequence *sequence = reinterpret_cast<olive::Sequence *>(seq);
|
|
if (!sequence || height <= 0.0 ||
|
|
track_type < OAKENGINE_TRACK_TYPE_VIDEO ||
|
|
track_type > OAKENGINE_TRACK_TYPE_SUBTITLE) {
|
|
set_seq_error(QStringLiteral("invalid arguments"));
|
|
return OAKENGINE_E_INVALID;
|
|
}
|
|
olive::TrackList *list =
|
|
sequence->track_list(to_track_type(track_type));
|
|
if (track_index < 0 || track_index >= list->get_track_count()) {
|
|
set_seq_error(QStringLiteral("no track at index %1")
|
|
.arg(track_index));
|
|
return OAKENGINE_E_NOT_FOUND;
|
|
}
|
|
// Straight setter like the application (not undoable).
|
|
list->get_track_at(track_index)->set_track_height(height);
|
|
return OAKENGINE_OK;
|
|
}
|
|
|
|
int oakengine_track_is_muted(const OakEngineSequence *seq, int track_type,
|
|
int track_index)
|
|
{
|
|
const olive::Sequence *sequence =
|
|
reinterpret_cast<const olive::Sequence *>(seq);
|
|
if (!sequence || track_type < OAKENGINE_TRACK_TYPE_VIDEO ||
|
|
track_type > OAKENGINE_TRACK_TYPE_SUBTITLE) {
|
|
return 0;
|
|
}
|
|
olive::TrackList *list =
|
|
sequence->track_list(to_track_type(track_type));
|
|
if (track_index < 0 || track_index >= list->get_track_count()) {
|
|
return 0;
|
|
}
|
|
return list->get_track_at(track_index)->is_muted() ? 1 : 0;
|
|
}
|
|
|
|
int oakengine_track_set_muted(OakEngineSequence *seq, int track_type,
|
|
int track_index, int muted)
|
|
{
|
|
set_seq_error(QString());
|
|
olive::Sequence *sequence = reinterpret_cast<olive::Sequence *>(seq);
|
|
if (!sequence || track_type < OAKENGINE_TRACK_TYPE_VIDEO ||
|
|
track_type > OAKENGINE_TRACK_TYPE_SUBTITLE) {
|
|
set_seq_error(QStringLiteral("invalid arguments"));
|
|
return OAKENGINE_E_INVALID;
|
|
}
|
|
olive::TrackList *list =
|
|
sequence->track_list(to_track_type(track_type));
|
|
if (track_index < 0 || track_index >= list->get_track_count()) {
|
|
set_seq_error(QStringLiteral("no track at index %1")
|
|
.arg(track_index));
|
|
return OAKENGINE_E_NOT_FOUND;
|
|
}
|
|
list->get_track_at(track_index)->set_muted(muted != 0);
|
|
return OAKENGINE_OK;
|
|
}
|
|
|
|
int oakengine_track_is_locked(const OakEngineSequence *seq, int track_type,
|
|
int track_index)
|
|
{
|
|
const olive::Sequence *sequence =
|
|
reinterpret_cast<const olive::Sequence *>(seq);
|
|
if (!sequence || track_type < OAKENGINE_TRACK_TYPE_VIDEO ||
|
|
track_type > OAKENGINE_TRACK_TYPE_SUBTITLE) {
|
|
return 0;
|
|
}
|
|
olive::TrackList *list =
|
|
sequence->track_list(to_track_type(track_type));
|
|
if (track_index < 0 || track_index >= list->get_track_count()) {
|
|
return 0;
|
|
}
|
|
return list->get_track_at(track_index)->is_locked() ? 1 : 0;
|
|
}
|
|
|
|
int oakengine_track_set_locked(OakEngineSequence *seq, int track_type,
|
|
int track_index, int locked)
|
|
{
|
|
set_seq_error(QString());
|
|
olive::Sequence *sequence = reinterpret_cast<olive::Sequence *>(seq);
|
|
if (!sequence || track_type < OAKENGINE_TRACK_TYPE_VIDEO ||
|
|
track_type > OAKENGINE_TRACK_TYPE_SUBTITLE) {
|
|
set_seq_error(QStringLiteral("invalid arguments"));
|
|
return OAKENGINE_E_INVALID;
|
|
}
|
|
olive::TrackList *list =
|
|
sequence->track_list(to_track_type(track_type));
|
|
if (track_index < 0 || track_index >= list->get_track_count()) {
|
|
set_seq_error(QStringLiteral("no track at index %1")
|
|
.arg(track_index));
|
|
return OAKENGINE_E_NOT_FOUND;
|
|
}
|
|
list->get_track_at(track_index)->set_locked(locked != 0);
|
|
return OAKENGINE_OK;
|
|
}
|
|
|
|
int oakengine_sequence_marker_add(OakEngineSequence *seq, int64_t time_ts,
|
|
const char *name)
|
|
{
|
|
set_seq_error(QString());
|
|
olive::Sequence *sequence = reinterpret_cast<olive::Sequence *>(seq);
|
|
if (!sequence) {
|
|
set_seq_error(QStringLiteral("invalid sequence"));
|
|
return OAKENGINE_E_INVALID;
|
|
}
|
|
olive::Rational tb;
|
|
if (!time_base_of(sequence, &tb)) {
|
|
set_seq_error(QStringLiteral("sequence has no valid frame rate"));
|
|
return OAKENGINE_E_STATE;
|
|
}
|
|
const olive::Rational time =
|
|
olive::core::Timecode::timestamp_to_time(time_ts, tb);
|
|
olive::TimelineMarkerList *markers = sequence->get_markers();
|
|
if (markers->get_marker_at_time(time)) {
|
|
// The engine's marker insertion asserts on duplicate times.
|
|
set_seq_error(QStringLiteral("a marker already exists at time %1")
|
|
.arg(time_ts));
|
|
return OAKENGINE_E_STATE;
|
|
}
|
|
push_or_run(new olive::MarkerAddCommand(
|
|
markers, olive::TimeRange(time, time),
|
|
QString::fromUtf8(name ? name : ""), 0),
|
|
QStringLiteral("Add Marker"));
|
|
return OAKENGINE_OK;
|
|
}
|
|
|
|
int oakengine_sequence_marker_remove(OakEngineSequence *seq, int64_t time_ts)
|
|
{
|
|
set_seq_error(QString());
|
|
olive::Sequence *sequence = reinterpret_cast<olive::Sequence *>(seq);
|
|
if (!sequence) {
|
|
set_seq_error(QStringLiteral("invalid sequence"));
|
|
return OAKENGINE_E_INVALID;
|
|
}
|
|
olive::Rational tb;
|
|
if (!time_base_of(sequence, &tb)) {
|
|
set_seq_error(QStringLiteral("sequence has no valid frame rate"));
|
|
return OAKENGINE_E_STATE;
|
|
}
|
|
const olive::Rational time =
|
|
olive::core::Timecode::timestamp_to_time(time_ts, tb);
|
|
olive::TimelineMarker *marker =
|
|
sequence->get_markers()->get_marker_at_time(time);
|
|
if (!marker) {
|
|
set_seq_error(QStringLiteral("no marker at time %1").arg(time_ts));
|
|
return OAKENGINE_E_NOT_FOUND;
|
|
}
|
|
push_or_run(new olive::MarkerRemoveCommand(marker),
|
|
QStringLiteral("Remove Marker"));
|
|
return OAKENGINE_OK;
|
|
}
|
|
|
|
int oakengine_sequence_marker_rename(OakEngineSequence *seq, int64_t time_ts,
|
|
const char *name)
|
|
{
|
|
set_seq_error(QString());
|
|
olive::Sequence *sequence = reinterpret_cast<olive::Sequence *>(seq);
|
|
if (!sequence) {
|
|
set_seq_error(QStringLiteral("invalid sequence"));
|
|
return OAKENGINE_E_INVALID;
|
|
}
|
|
olive::Rational tb;
|
|
if (!time_base_of(sequence, &tb)) {
|
|
set_seq_error(QStringLiteral("sequence has no valid frame rate"));
|
|
return OAKENGINE_E_STATE;
|
|
}
|
|
const olive::Rational time =
|
|
olive::core::Timecode::timestamp_to_time(time_ts, tb);
|
|
olive::TimelineMarker *marker =
|
|
sequence->get_markers()->get_marker_at_time(time);
|
|
if (!marker) {
|
|
set_seq_error(QStringLiteral("no marker at time %1").arg(time_ts));
|
|
return OAKENGINE_E_NOT_FOUND;
|
|
}
|
|
push_or_run(new olive::MarkerChangeNameCommand(
|
|
marker, QString::fromUtf8(name ? name : "")),
|
|
QStringLiteral("Rename Marker"));
|
|
return OAKENGINE_OK;
|
|
}
|
|
|
|
} // extern "C"
|