diff --git a/engine/CMakeLists.txt b/engine/CMakeLists.txt
index a15abe5cf..13b7612a3 100644
--- a/engine/CMakeLists.txt
+++ b/engine/CMakeLists.txt
@@ -51,6 +51,13 @@ add_library(oakengine SHARED
${OLIVE_RESOURCES}
)
+# macOS: hides the render worker's dock icon; called from the worker main in
+# src/capi/worker.cpp (declared there as a plain C++ symbol).
+if (APPLE)
+ target_sources(oakengine PRIVATE src/worker_dockicon_mac.mm)
+ target_link_libraries(oakengine PRIVATE "-framework Cocoa")
+endif ()
+
add_subdirectory(common)
add_subdirectory(pluginSupport)
@@ -61,7 +68,7 @@ set_target_properties(oakengine PROPERTIES
)
# Consumers resolve engine headers ("node/...", "render/...", "coreengine.h",
-# "ui/icons/icons.h", "tool/tool.h") from the engine root, and the public C
+# "tool/tool.h") from the engine root, and the public C
# API ("oakengine/ipc.h") from include/. The library itself builds against its
# internal implementation headers under src/oliveimpl, included with an
# "oliveimpl/"-prefixed path resolved from src/ (mirrors the src/oliveimpl
@@ -227,13 +234,47 @@ if (BUILD_TESTS)
make_oakengine_test(oakengine_ipc_test)
+ make_oakengine_test(oakengine_worker_test)
+
make_oakengine_test(oakengine_init_test)
+
+ make_oakengine_test(oakengine_app_test)
# Resolves the real test assets (tests/demo.mp4, the footage fixture
# project) relative to the repository root, like tests/gtest does.
target_compile_definitions(oakengine_init_test PRIVATE
OAK_TEST_SOURCE_DIR="${CMAKE_SOURCE_DIR}"
)
+ make_oakengine_test(oakengine_task_test)
+ # The task test creates temporary projects and imports non-existent files,
+ # so it only needs the headless engine services.
+
+ make_oakengine_test(oakengine_config_test)
+ # The config test exercises the QSettings-backed key/value store through
+ # the C ABI; it is headless and does not touch the disk cache.
+
+ make_oakengine_test(oakengine_audio_test)
+ # The audio test exercises the AudioManager instance lifecycle, device
+ # get/set round-trips and the output_params_changed event. It is headless
+ # and only needs PortAudio initialization.
+
+ make_oakengine_test(oakengine_disk_test)
+ # The disk test exercises the DiskManager instance lifecycle, default cache
+ # path queries, cache clearing, settings handler dispatch and default path
+ # mutation. It is headless and only touches temporary directories.
+
+ make_oakengine_test(oakengine_proxy_test)
+ # The proxy test exercises proxy parameter defaults, state string
+ # round-trips and the ProxyManager singleton lifecycle. It is headless.
+
+ make_oakengine_test(oakengine_lut_test)
+ # The LUT test exercises directory/file list queries and the
+ # set_directories round-trip. It is headless.
+
+ make_oakengine_test(oakengine_serializer_test)
+ # The serializer test exercises compressed-project detection, clipboard
+ # create/free/copy and empty-node copy. It is headless.
+
make_oakengine_test(oakengine_renderer_test)
# The renderer test builds sequence content through the engine C++ API
# (allowed for engine-internal tests) and probes the dynamic render
@@ -275,6 +316,15 @@ if (BUILD_TESTS)
OAK_TEST_SOURCE_DIR="${CMAKE_SOURCE_DIR}"
)
+ make_oakengine_test(oakengine_events_test)
+ target_compile_definitions(oakengine_events_test PRIVATE
+ OAK_TEST_SOURCE_DIR="${CMAKE_SOURCE_DIR}"
+ )
+
+ make_oakengine_test(oakengine_encoding_test)
+
+ make_oakengine_test(oakengine_color_test)
+
make_oakengine_test(oakengine_export_test)
# The export test builds sequence content through the engine C++ API and
# probes the dynamic render backend like oakengine_renderer_test does.
@@ -303,6 +353,8 @@ if (BUILD_TESTS)
endif ()
make_oakengine_test(oakengine_node_test)
+
+ make_oakengine_test(oakengine_nodevalue_test)
target_compile_definitions(oakengine_node_test PRIVATE
OAK_TEST_SOURCE_DIR="${CMAKE_SOURCE_DIR}"
)
@@ -312,6 +364,16 @@ if (BUILD_TESTS)
OAK_TEST_SOURCE_DIR="${CMAKE_SOURCE_DIR}"
)
+ make_oakengine_test(oakengine_viewer_test)
+ target_compile_definitions(oakengine_viewer_test PRIVATE
+ OAK_TEST_SOURCE_DIR="${CMAKE_SOURCE_DIR}"
+ )
+
+ make_oakengine_test(oakengine_traverse_test)
+ target_compile_definitions(oakengine_traverse_test PRIVATE
+ OAK_TEST_SOURCE_DIR="${CMAKE_SOURCE_DIR}"
+ )
+
make_oakengine_test(oakengine_preview_test)
# The preview test needs audio rendering (RenderManager + workers).
target_include_directories(oakengine_preview_test PRIVATE
@@ -362,4 +424,31 @@ if (BUILD_TESTS)
if (TARGET olive-render-worker)
add_dependencies(oakengine_playback_test olive-render-worker)
endif ()
+
+ make_oakengine_test(oakengine_sync_test)
+ # The sync test renders the clips' audio through the worker pool (same
+ # needs as oakengine_playback_test).
+ target_include_directories(oakengine_sync_test PRIVATE
+ ${CMAKE_SOURCE_DIR}/third_party/openfx/include
+ ${CMAKE_SOURCE_DIR}/third_party/openfx/HostSupport/include
+ ${OLIVE_INCLUDE_DIRS}
+ )
+ target_compile_definitions(oakengine_sync_test PRIVATE
+ ${OLIVE_DEFINITIONS}
+ OAK_TEST_SOURCE_DIR="${CMAKE_SOURCE_DIR}"
+ )
+ target_compile_options(oakengine_sync_test PRIVATE
+ ${OLIVE_COMPILE_OPTIONS}
+ )
+ if (OAK_ENABLE_DYNAMIC_RENDER_BACKEND)
+ target_compile_definitions(oakengine_sync_test PRIVATE
+ OAK_ENABLE_DYNAMIC_RENDER_BACKEND)
+ add_dependencies(oakengine_sync_test oakgl)
+ if (TARGET oakvulkan)
+ add_dependencies(oakengine_sync_test oakvulkan)
+ endif ()
+ endif ()
+ if (TARGET olive-render-worker)
+ add_dependencies(oakengine_sync_test olive-render-worker)
+ endif ()
endif ()
diff --git a/engine/include/oakengine/app.h b/engine/include/oakengine/app.h
new file mode 100644
index 000000000..91a1e39d1
--- /dev/null
+++ b/engine/include/oakengine/app.h
@@ -0,0 +1,463 @@
+/***
+
+ 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 .
+
+***/
+
+#ifndef OAKENGINE_APP_H
+#define OAKENGINE_APP_H
+
+#include "export.h"
+#include "footage.h"
+#include "init.h"
+#include "project.h"
+
+#ifdef __cplusplus
+extern "C" {
+#endif
+
+/**
+ * @file app.h
+ * @brief C ABI for the application-level engine state (EngineCore facade)
+ *
+ * This family exposes the process-wide application state the editor UI needs
+ * from the engine: the CoreParams-driven startup, the open/active project,
+ * the recent-projects list, the global tool/snapping/timecode settings, the
+ * status bar and the UI handler hooks the engine calls when it needs user
+ * interaction (image-sequence confirmation, footage relink, project save /
+ * close, main window layout restore, OTIO import).
+ *
+ * It wraps olive::EngineCore so that the UI layer no longer derives from or
+ * links against that C++ class. The engine emits its change notifications
+ * through the OakEngineAppCallbacks function pointers (registered with
+ * oakengine_app_set_callbacks()) instead of Qt signals.
+ *
+ * Conventions (matching oakengine/project.h):
+ * - Booleans are int (1/0).
+ * - Return codes: 0 (OAKENGINE_OK) on success, negative OAKENGINE_E_* on
+ * failure. Functions documented as returning a value return
+ * OAKENGINE_E_INVALID when no application core exists.
+ * - String output uses the buf/size convention: the return value is the
+ * number of characters that would have been written excluding the NUL,
+ * so buf == NULL or a short buffer queries the required size. The output
+ * is NUL-terminated whenever buf_size > 0. A negative return value is an
+ * OAKENGINE_E_* error code.
+ * - Enum values mirror the engine enums (olive::Tool::Item,
+ * olive::Tool::AddableObject, olive::core::Timecode::Display) and are
+ * passed as plain int; the numeric values are identical.
+ */
+
+/**
+ * @brief Application run modes (mirrors olive::EngineCore::CoreParams::RunMode).
+ */
+#define OAKENGINE_APP_RUN_NORMAL 0 /**< Normal GUI run. */
+#define OAKENGINE_APP_RUN_HEADLESS_EXPORT 1 /**< Export without GUI. */
+#define OAKENGINE_APP_RUN_HEADLESS_PRE_CACHE 2 /**< Pre-cache without GUI. */
+
+/**
+ * @brief Startup parameters for oakengine_app_create().
+ *
+ * Strings may be NULL (treated as empty). The struct is copied by
+ * oakengine_app_create(); the pointed-to strings are only read during the
+ * call.
+ */
+typedef struct OakEngineAppParams {
+ int run_mode; /**< OAKENGINE_APP_RUN_* value. */
+ int fullscreen; /**< Start the main window fullscreen (bool). */
+ const char *startup_project; /**< Project file to open on startup, or NULL. */
+ const char *startup_language; /**< .qm file overriding the language, or NULL. */
+ int crash_on_startup; /**< Trigger a manual crash shortly after start (bool). */
+} OakEngineAppParams;
+
+/**
+ * @brief UI handler and notification callback set.
+ *
+ * Any field may be NULL. A NULL handler makes the engine fall back to its
+ * headless default (accept the import, close without prompting, skip the
+ * file write); a NULL notification simply drops the event.
+ *
+ * The callbacks are invoked synchronously on the thread that triggered the
+ * engine call (usually the main thread). `userdata` is passed back verbatim.
+ *
+ * `load_layout` receives a `const olive::SerializedLayoutInfo *` (engine
+ * data structure, only valid during the call). `otio_import` receives an
+ * array of borrowed olive::Sequence pointers as OakEngineSequence handles.
+ */
+typedef struct OakEngineAppCallbacks {
+ void *userdata;
+
+ /* UI handlers (engine asks the application) */
+ int (*confirm_image_sequence)(const char *filename, void *userdata);
+ int (*relink_footage)(OakEngineFootage **footage, int count,
+ void *userdata);
+ void (*save_project)(const char *override_filename, void *userdata);
+ int (*close_project)(void *userdata);
+ void (*load_layout)(const void *layout, void *userdata);
+ int (*otio_import)(OakEngineSequence **sequences, int count,
+ void *userdata);
+
+ /* Notifications (engine informs the application) */
+ void (*status_message_show)(const char *message, int timeout,
+ void *userdata);
+ void (*status_message_clear)(void *userdata);
+ void (*cache_full_warning)(void *userdata);
+ void (*active_project_changed)(OakEngineProject *project, void *userdata);
+ void (*tool_changed)(int tool, void *userdata);
+ void (*addable_object_changed)(int object, void *userdata);
+ void (*snapping_changed)(int snapping, void *userdata);
+ void (*timecode_display_changed)(int display, void *userdata);
+ void (*open_recent_list_changed)(void *userdata);
+ void (*color_picker_enabled)(int enabled, void *userdata);
+} OakEngineAppCallbacks;
+
+/**
+ * @brief Create the application engine core with the given startup params.
+ *
+ * `params` may be NULL for defaults (normal run, no startup project). Only
+ * one application core may exist per process: if one already exists (either
+ * from an earlier oakengine_app_create() or from the EngineCore shell that
+ * oakengine_init() creates), OAKENGINE_E_STATE is returned.
+ *
+ * The core is never destroyed; it backs the process-wide engine singleton.
+ *
+ * @return OAKENGINE_OK on success, OAKENGINE_E_STATE if a core exists.
+ */
+OAKENGINE_API int oakengine_app_create(const OakEngineAppParams *params);
+
+/**
+ * @brief Start the engine services for the application (config, locale,
+ * managers, autorecovery timer, recent projects list).
+ *
+ * @return OAKENGINE_OK on success, OAKENGINE_E_STATE if no core exists or
+ * the application core was already started.
+ */
+OAKENGINE_API int oakengine_app_start(void);
+
+/**
+ * @brief Stop the engine services started by oakengine_app_start().
+ *
+ * @return OAKENGINE_OK on success, OAKENGINE_E_STATE if the application
+ * core was not started.
+ */
+OAKENGINE_API int oakengine_app_stop(void);
+
+/**
+ * @brief Register the UI handler/notification callback set.
+ *
+ * The struct is copied; NULL clears all callbacks and restores the headless
+ * default behavior.
+ *
+ * @return OAKENGINE_OK.
+ */
+OAKENGINE_API int
+oakengine_app_set_callbacks(const OakEngineAppCallbacks *callbacks);
+
+/**
+ * @brief Startup parameter accessors (valid once a core exists).
+ *
+ * oakengine_app_run_mode() returns an OAKENGINE_APP_RUN_* value,
+ * oakengine_app_fullscreen() a boolean; both return OAKENGINE_E_INVALID when
+ * no core exists. oakengine_app_startup_project() uses the buf/size
+ * convention.
+ */
+OAKENGINE_API int oakengine_app_run_mode(void);
+OAKENGINE_API int oakengine_app_fullscreen(void);
+OAKENGINE_API int oakengine_app_startup_project(char *buf, int buf_size);
+
+/**
+ * @brief Process-wide undo stack as an opaque pointer (an
+ * olive::UndoStack *). Returns NULL when no core exists.
+ */
+OAKENGINE_API void *oakengine_app_undo_stack(void);
+
+/**
+ * @brief Current tool as an olive::Tool::Item value (int).
+ */
+OAKENGINE_API int oakengine_app_tool(void);
+
+/**
+ * @brief Set the current tool. Valid values are 0 <= tool < Tool::k_count.
+ * Emits the tool_changed notification.
+ */
+OAKENGINE_API int oakengine_app_set_tool(int tool);
+
+/**
+ * @brief Currently selected addable object (olive::Tool::AddableObject).
+ */
+OAKENGINE_API int oakengine_app_addable_object(void);
+
+/**
+ * @brief Set the addable object. Valid values are 0 <= object <
+ * Tool::k_addable_count. Emits addable_object_changed.
+ */
+OAKENGINE_API int oakengine_app_set_addable_object(int object);
+
+/**
+ * @brief Currently selected transition id (buf/size convention).
+ */
+OAKENGINE_API int oakengine_app_selected_transition(char *buf, int buf_size);
+
+/**
+ * @brief Set the selected transition id (NULL clears it).
+ */
+OAKENGINE_API int oakengine_app_set_selected_transition(const char *id);
+
+/**
+ * @brief Current snapping setting (boolean).
+ */
+OAKENGINE_API int oakengine_app_snapping(void);
+
+/**
+ * @brief Set snapping. Emits snapping_changed.
+ */
+OAKENGINE_API int oakengine_app_set_snapping(int enabled);
+
+/**
+ * @brief Current timecode display mode (olive::core::Timecode::Display).
+ */
+OAKENGINE_API int oakengine_app_timecode_display(void);
+
+/**
+ * @brief Set the timecode display mode (0 <= display <= 4). Emits
+ * timecode_display_changed.
+ */
+OAKENGINE_API int oakengine_app_set_timecode_display(int display);
+
+/**
+ * @brief Number of entries in the recently opened/saved projects list.
+ */
+OAKENGINE_API int oakengine_app_recent_projects_count(void);
+
+/**
+ * @brief Path of the recent-project entry at `index` (buf/size convention).
+ *
+ * @return the string length, or OAKENGINE_E_NOT_FOUND for an invalid index.
+ */
+OAKENGINE_API int oakengine_app_recent_project_at(int index, char *buf,
+ int buf_size);
+
+/**
+ * @brief Remove the recent-project entry at `index`. Emits
+ * open_recent_list_changed.
+ *
+ * @return OAKENGINE_OK, or OAKENGINE_E_NOT_FOUND for an invalid index.
+ */
+OAKENGINE_API int oakengine_app_remove_recent_project(int index);
+
+/**
+ * @brief Clear the recent projects list. Emits open_recent_list_changed.
+ */
+OAKENGINE_API int oakengine_app_clear_recent_projects(void);
+
+/**
+ * @brief Show a message in the status bar (delivered through the
+ * status_message_show callback).
+ */
+OAKENGINE_API int oakengine_app_show_status_message(const char *message,
+ int timeout);
+
+/**
+ * @brief Clear the status bar (delivered through the status_message_clear
+ * callback).
+ */
+OAKENGINE_API int oakengine_app_clear_status_message(void);
+
+/**
+ * @brief Change the current language.
+ *
+ * @return 1 if a translation for `locale` was found and installed, 0 if
+ * not, OAKENGINE_E_INVALID for NULL or when no core exists.
+ */
+OAKENGINE_API int oakengine_app_set_language(const char *locale);
+
+/**
+ * @brief Set how frequently an autorecovery is saved (minutes).
+ */
+OAKENGINE_API int oakengine_app_set_autorecovery_interval(int minutes);
+
+/**
+ * @brief Globally enable/disable decoding from proxy media.
+ */
+OAKENGINE_API int oakengine_app_set_use_proxy_media(int enabled);
+
+/**
+ * @brief Add/remove a pixel-sampling user. Emits color_picker_enabled when
+ * the user count crosses 0.
+ */
+OAKENGINE_API int oakengine_app_request_pixel_sampling(int enable);
+
+/**
+ * @brief Debug "magic" flag accessors.
+ */
+OAKENGINE_API int oakengine_app_set_magic(int enabled);
+OAKENGINE_API int oakengine_app_is_magic_enabled(void);
+
+/**
+ * @brief Copy a string to the system clipboard.
+ */
+OAKENGINE_API int oakengine_app_copy_to_clipboard(const char *text);
+
+/**
+ * @brief Paste a string from the system clipboard (buf/size convention).
+ */
+OAKENGINE_API int oakengine_app_paste_from_clipboard(char *buf, int buf_size);
+
+/**
+ * @brief File filter for footage import dialogs (buf/size convention).
+ */
+OAKENGINE_API int oakengine_app_footage_file_dialog_filter(char *buf,
+ int buf_size);
+
+/**
+ * @brief Whether `path` has an extension allowed for footage import.
+ *
+ * @return 1/0, or OAKENGINE_E_INVALID for NULL.
+ */
+OAKENGINE_API int oakengine_app_is_footage_extension_allowed(const char *path);
+
+/**
+ * @brief Create a new sequence named appropriately for `project`.
+ *
+ * `name_format` is a QString::arg() pattern (e.g. "Sequence %1"); NULL uses
+ * the default "Sequence %1". The returned handle is owned by the caller
+ * (it is not yet added to the project). Returns NULL on invalid input.
+ */
+OAKENGINE_API OakEngineSequence *
+oakengine_app_create_sequence(OakEngineProject *project,
+ const char *name_format);
+
+/**
+ * @brief Path of the autorecovery index file (buf/size convention).
+ */
+OAKENGINE_API int oakengine_app_auto_recovery_index_filename(char *buf,
+ int buf_size);
+
+/**
+ * @brief Currently open project (borrowed handle, may be NULL).
+ */
+OAKENGINE_API OakEngineProject *oakengine_app_open_project(void);
+
+/**
+ * @brief Close the current project (through the close_project handler) and
+ * open a new empty one.
+ */
+OAKENGINE_API int oakengine_app_create_new_project(void);
+
+/**
+ * @brief Open an already-loaded project, closing the current one first.
+ * Pushes it to the recent list when `add_to_recents` is set and the project
+ * has a filename.
+ */
+OAKENGINE_API int oakengine_app_add_open_project(OakEngineProject *project,
+ int add_to_recents);
+
+/**
+ * @brief Adopt the project loaded by a project-load task (an olive::Task *
+ * as an opaque pointer).
+ *
+ * @return 1 if the project was opened, 0 if the load was cancelled or the
+ * footage validation was rejected, OAKENGINE_E_INVALID for NULL.
+ */
+OAKENGINE_API int oakengine_app_add_open_project_from_task(void *task,
+ int add_to_recents);
+
+/**
+ * @brief Adopt an autorecovery project loaded by a project-load task (an
+ * olive::Task * as an opaque pointer).
+ *
+ * @return 1 on success, 0 otherwise, OAKENGINE_E_INVALID for NULL.
+ */
+OAKENGINE_API int oakengine_app_add_recovery_project_from_task(void *task);
+
+/**
+ * @brief Update engine state after `project` was successfully saved (recent
+ * list, modified flag, unrecovered list).
+ */
+OAKENGINE_API int oakengine_app_on_project_saved(OakEngineProject *project);
+
+/**
+ * @brief Set the active (open) project. Emits active_project_changed.
+ * `project` may be NULL.
+ */
+OAKENGINE_API int oakengine_app_set_active_project(OakEngineProject *project);
+
+/**
+ * @brief Convenience wrapper: set just the confirm-image-sequence handler
+ * (same as setting cb.confirm_image_sequence in oakengine_app_set_callbacks).
+ * Replaces both fn and userdata.
+ */
+OAKENGINE_API int oakengine_app_set_confirm_image_sequence_handler(
+ int (*fn)(const char *filename, void *userdata), void *userdata);
+
+/**
+ * @brief Convenience wrapper: set just the relink handler.
+ */
+OAKENGINE_API int oakengine_app_set_relink_handler(
+ int (*fn)(OakEngineFootage **footage, int count, void *userdata),
+ void *userdata);
+
+/**
+ * @brief Convenience wrapper: set just the save-project handler.
+ */
+OAKENGINE_API int oakengine_app_set_save_project_handler(
+ void (*fn)(const char *override_filename, void *userdata), void *userdata);
+
+/**
+ * @brief Convenience wrapper: set just the close-project handler.
+ */
+OAKENGINE_API int oakengine_app_set_close_project_handler(
+ int (*fn)(void *userdata), void *userdata);
+
+/**
+ * @brief Convenience wrapper: set just the load-layout handler.
+ */
+OAKENGINE_API int oakengine_app_set_load_layout_handler(
+ void (*fn)(const void *layout, void *userdata), void *userdata);
+
+/**
+ * @brief Alias for oakengine_app_auto_recovery_index_filename().
+ */
+OAKENGINE_API int oakengine_app_get_auto_recovery_index_filename(char *buf,
+ int buf_size);
+
+/**
+ * @brief Alias for oakengine_app_remove_recent_project().
+ */
+OAKENGINE_API int oakengine_app_remove_recently_opened_project(int index);
+
+/**
+ * @brief void*-based overload of oakengine_app_on_project_saved() for use
+ * from app code that holds a opaque QObject pointer.
+ */
+OAKENGINE_API int oakengine_app_on_project_saved_vp(void *project);
+
+/**
+ * @brief void*-based overload of oakengine_app_set_active_project().
+ */
+OAKENGINE_API int oakengine_app_set_active_project_vp(void *project);
+
+/**
+ * @brief void*-based overload of oakengine_app_add_open_project().
+ */
+OAKENGINE_API int oakengine_app_add_open_project_vp(void *project,
+ int add_to_recents);
+
+#ifdef __cplusplus
+}
+#endif
+
+#endif /* OAKENGINE_APP_H */
diff --git a/engine/include/oakengine/audio.h b/engine/include/oakengine/audio.h
new file mode 100644
index 000000000..306d2f298
--- /dev/null
+++ b/engine/include/oakengine/audio.h
@@ -0,0 +1,330 @@
+/***
+
+ 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 .
+
+***/
+
+#ifndef OAKENGINE_AUDIO_H
+#define OAKENGINE_AUDIO_H
+
+#include
+
+#include "export.h"
+#include "encoding.h"
+#include "init.h"
+
+#ifdef __cplusplus
+extern "C" {
+#endif
+
+/**
+ * @file audio.h
+ * @brief C ABI for the engine's audio I/O singleton (olive::AudioManager)
+ *
+ * A thin facade over AudioManager's instance lifecycle, input/output device
+ * selection, output buffer management and recording stop control. The
+ * AudioManager handle returned by oakengine_audio_manager_handle() is a
+ * borrowed opaque pointer intended only for event subscription
+ * (OAKENGINE_EVENT_AUDIO_MANAGER_OUTPUT_PARAMS_CHANGED); it is not a general
+ * purpose object handle and must not be freed.
+ *
+ * Conventions match the other facade families:
+ * - 0 (OAKENGINE_OK) / negative OAKENGINE_E_* codes.
+ * - Device indices are PortAudio PaDeviceIndex values (int64_t across the
+ * boundary); paNoDevice is -1.
+ * - String output uses the buf/size convention (error_buf for
+ * oakengine_audio_push_to_output).
+ */
+
+typedef struct OakAudioParams OakAudioParams;
+
+/**
+ * @brief Create the AudioManager singleton.
+ *
+ * Safe to call when the instance already exists (no-op). Returns
+ * OAKENGINE_OK or OAKENGINE_E_FAILED.
+ */
+OAKENGINE_API int oakengine_audio_create_instance(void);
+
+/**
+ * @brief Destroy the AudioManager singleton.
+ *
+ * Safe to call when no instance exists (no-op). Returns OAKENGINE_OK.
+ */
+OAKENGINE_API int oakengine_audio_destroy_instance(void);
+
+/**
+ * @brief Borrowed handle to the AudioManager singleton, or NULL if none.
+ *
+ * Intended only for subscribing to
+ * OAKENGINE_EVENT_AUDIO_MANAGER_OUTPUT_PARAMS_CHANGED. The pointer is owned
+ * by the engine and becomes NULL after oakengine_audio_destroy_instance().
+ */
+OAKENGINE_API void *oakengine_audio_manager_handle(void);
+
+/**
+ * @brief Current output device index (paNoDevice = -1 when none).
+ *
+ * Returns the current value from the AudioManager singleton, or paNoDevice if
+ * no instance exists.
+ */
+OAKENGINE_API int64_t oakengine_audio_get_output_device(void);
+
+/**
+ * @brief Set the output device index.
+ *
+ * Changing the device may emit output_params_changed. Returns OAKENGINE_OK or
+ * OAKENGINE_E_FAILED.
+ */
+OAKENGINE_API int oakengine_audio_set_output_device(int64_t device);
+
+/**
+ * @brief Current input device index (paNoDevice = -1 when none).
+ */
+OAKENGINE_API int64_t oakengine_audio_get_input_device(void);
+
+/**
+ * @brief Set the input device index.
+ */
+OAKENGINE_API int oakengine_audio_set_input_device(int64_t device);
+
+/**
+ * @brief Re-initialize PortAudio and refresh the device lists.
+ */
+OAKENGINE_API int oakengine_audio_hard_reset(void);
+
+/**
+ * @brief Clear any buffered output samples.
+ */
+OAKENGINE_API int oakengine_audio_clear_buffered_output(void);
+
+/**
+ * @brief Push a packed sample buffer to the current output device.
+ *
+ * `params` is an owned or borrowed OakAudioParams handle describing the
+ * sample data. `samples` points to `samples_size` bytes of interleaved audio
+ * data in the format described by `params`. On failure a human-readable
+ * message is written into `error_buf` (up to `error_buf_size` bytes including
+ * the terminating NUL) and OAKENGINE_E_FAILED is returned.
+ */
+OAKENGINE_API int oakengine_audio_push_to_output(const OakAudioParams *params,
+ const char *samples,
+ int64_t samples_size,
+ char *error_buf,
+ int error_buf_size);
+
+/**
+ * @brief Stop an active recording session.
+ */
+OAKENGINE_API int oakengine_audio_stop_recording(void);
+
+/**
+ * @brief Stop audio output.
+ */
+OAKENGINE_API int oakengine_audio_stop_output(void);
+
+/**
+ * @brief Restart the output clock at zero for a new playback run.
+ */
+OAKENGINE_API int oakengine_audio_reset_output_clock(void);
+
+/**
+ * @brief Set the output notify interval in bytes.
+ *
+ * After this many bytes of audio have been consumed by the output device,
+ * the AudioManager emits output_notify (which translates to the
+ * OAKENGINE_EVENT_AUDIO_MANAGER_OUTPUT_NOTIFY event for C subscribers).
+ */
+OAKENGINE_API int oakengine_audio_set_output_notify_interval(int64_t bytes);
+
+/**
+ * @brief Start audio recording.
+ *
+ * Takes ownership of `params`: the handle is destroyed when the recording
+ * ends. On failure a human-readable message is written into `error_buf`
+ * (up to `error_buf_size` bytes including the terminating NUL).
+ *
+ * @return OAKENGINE_OK on success, OAKENGINE_E_FAILED on error.
+ */
+OAKENGINE_API int oakengine_audio_start_recording(
+ OakEngineEncodingParams *params, char *error_buf, int error_buf_size);
+
+/* ---- Audio synchronization (R6 P1.3) ------------------------------------ */
+
+/** @brief Result of envelope-offset correlation. */
+typedef struct oak_audio_waveform_offset {
+ int64_t offset_samples;
+ double confidence;
+ /** 1 if the offset is usable, 0 otherwise. */
+ int valid;
+} oak_audio_waveform_offset;
+
+/** @brief Result of rate+offset correlation. */
+typedef struct oak_audio_waveform_stretch_offset {
+ double rate;
+ int64_t offset_samples;
+ double confidence;
+ /** 1 if the result is usable, 0 otherwise. */
+ int valid;
+} oak_audio_waveform_stretch_offset;
+
+/**
+ * @brief Estimate the sample offset between two RMS envelopes.
+ *
+ * `reference_valid`/`candidate_valid` may be NULL to mean "all windows valid";
+ * if non-NULL their lengths must equal `reference_len`/`candidate_len`.
+ *
+ * @return OAKENGINE_OK with `out` filled, or OAKENGINE_E_INVALID.
+ */
+OAKENGINE_API int oakengine_audio_estimate_envelope_offset(
+ const double *reference, int reference_len,
+ const double *candidate, int candidate_len,
+ const bool *reference_valid, int reference_valid_len,
+ const bool *candidate_valid, int candidate_valid_len,
+ uint64_t window_samples, int64_t max_offset_windows,
+ oak_audio_waveform_offset *out);
+
+/**
+ * @brief Estimate a playback-rate change plus offset aligning candidate to
+ * reference.
+ *
+ * See AudioWaveformSync::estimate_stretch_and_offset().
+ */
+OAKENGINE_API int oakengine_audio_estimate_stretch_and_offset(
+ const double *reference, int reference_len,
+ const double *candidate, int candidate_len,
+ const bool *reference_valid, int reference_valid_len,
+ const bool *candidate_valid, int candidate_valid_len,
+ uint64_t window_samples, int64_t max_offset_windows,
+ double min_rate, double max_rate, double rate_step,
+ oak_audio_waveform_stretch_offset *out);
+
+/** @brief Source-clip description for source-time synchronization. */
+typedef struct oak_audio_sync_source_clip {
+ /** Source start time as a Rational num/den pair. */
+ int64_t source_start_time_num;
+ int64_t source_start_time_den;
+ /** Media in-point as a Rational num/den pair. */
+ int64_t media_in_num;
+ int64_t media_in_den;
+ /** 1 if source_start_time is meaningful, 0 otherwise. */
+ int has_source_start_time;
+} oak_audio_sync_source_clip;
+
+/** @brief Timeline placement result from AudioSynchronizer. */
+typedef struct oak_audio_sync_placement {
+ /** Timeline in-point as a Rational num/den pair. */
+ int64_t timeline_in_num;
+ int64_t timeline_in_den;
+ /** 1 if the placement is usable, 0 otherwise. */
+ int valid;
+} oak_audio_sync_placement;
+
+/**
+ * @brief Compute a candidate clip's timeline placement from source timecodes.
+ *
+ * `reference_timeline_in` is the reference clip's timeline in-point as a
+ * Rational num/den pair.
+ */
+OAKENGINE_API int oakengine_audio_sync_place_by_source_time(
+ const oak_audio_sync_source_clip *reference,
+ const oak_audio_sync_source_clip *candidate,
+ int64_t reference_timeline_in_num, int64_t reference_timeline_in_den,
+ oak_audio_sync_placement *out);
+
+/**
+ * @brief Compute a candidate clip's timeline placement from a waveform offset.
+ */
+OAKENGINE_API int oakengine_audio_sync_place_by_waveform_offset(
+ int64_t reference_timeline_in_num, int64_t reference_timeline_in_den,
+ int64_t candidate_offset_samples, int sample_rate,
+ oak_audio_sync_placement *out);
+
+/* ---- Audio format processor (R6 P5) ------------------------------------- */
+
+/**
+ * @brief Opaque audio format converter (olive::AudioProcessor).
+ *
+ * Converts planar float samples from one format to a packed output format,
+ * optionally applying tempo (speed) scaling. Used by the viewer to feed the
+ * audio output device. Create with oakengine_audio_processor_create() and
+ * destroy with oakengine_audio_processor_free().
+ */
+typedef struct OakEngineAudioProcessor OakEngineAudioProcessor;
+
+/**
+ * @brief Create an audio processor with no open graph.
+ *
+ * Returns NULL on allocation failure.
+ */
+OAKENGINE_API OakEngineAudioProcessor *oakengine_audio_processor_create(void);
+
+/**
+ * @brief Destroy the processor, closing any open graph. Safe to call with
+ * NULL.
+ */
+OAKENGINE_API void oakengine_audio_processor_free(OakEngineAudioProcessor *p);
+
+/**
+ * @brief Open the conversion graph.
+ *
+ * `from` describes the planar float input format and `to` the packed output
+ * format (both borrowed handles, copied internally). `tempo` is the playback
+ * speed (1.0 = normal). The processor must not already be open. Returns
+ * OAKENGINE_OK, OAKENGINE_E_INVALID, or OAKENGINE_E_FAILED.
+ */
+OAKENGINE_API int oakengine_audio_processor_open(OakEngineAudioProcessor *p,
+ const OakAudioParams *from, const OakAudioParams *to, double tempo);
+
+/**
+ * @brief Close the conversion graph. Safe to call when not open or with
+ * NULL.
+ */
+OAKENGINE_API void oakengine_audio_processor_close(OakEngineAudioProcessor *p);
+
+/**
+ * @brief 1 if the processor has an open graph, 0 otherwise (or NULL).
+ */
+OAKENGINE_API int oakengine_audio_processor_is_open(OakEngineAudioProcessor *p);
+
+/**
+ * @brief Convert planar float samples to the packed output format.
+ *
+ * `in` is an array of per-channel float pointers (channel count as given to
+ * open()); `nb_in_samples` is the number of frames. On success (>= 0),
+ * `*out_data` points to the packed output bytes owned by `p` (valid until the
+ * next convert/close/free) and `*out_size` holds the byte count, which may be
+ * 0 when the tempo buffer absorbed the block. Returns a negative error code
+ * on failure.
+ */
+OAKENGINE_API int oakengine_audio_processor_convert(OakEngineAudioProcessor *p,
+ float **in, int nb_in_samples, const void **out_data, int *out_size);
+
+/**
+ * @brief Output (packed) parameters as a new OakAudioParams handle.
+ *
+ * The caller owns the result and must free it with oakcore_audioparams_free().
+ * Returns NULL if `p` is NULL or not open.
+ */
+OAKENGINE_API OakAudioParams *oakengine_audio_processor_output_params(
+ OakEngineAudioProcessor *p);
+
+#ifdef __cplusplus
+}
+#endif
+
+#endif /* OAKENGINE_AUDIO_H */
diff --git a/engine/include/oakengine/color.h b/engine/include/oakengine/color.h
new file mode 100644
index 000000000..d3b9ac986
--- /dev/null
+++ b/engine/include/oakengine/color.h
@@ -0,0 +1,297 @@
+/***
+
+ 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 .
+
+***/
+
+#ifndef OAKENGINE_COLOR_H
+#define OAKENGINE_COLOR_H
+
+#include "export.h"
+#include "init.h"
+#include "project.h"
+
+#ifdef __cplusplus
+extern "C" {
+#endif
+
+/**
+ * @file color.h
+ * @brief C ABI for color management (the olive::ColorManager /
+ * ColorTransform / ColorProcessor surface)
+ *
+ * Covers everything the application's display and color-picker paths need
+ * without importing an engine C++ symbol:
+ *
+ * - OakEngineColorManager: borrowed handle to a project's color manager
+ * (olive::ColorManager). Obtain it with
+ * oakengine_color_manager_from_project(); like the other borrowed
+ * handles it is just the engine pointer reinterpreted and its lifetime
+ * follows the project. All list queries use the index + buf/size
+ * string pattern (the return value of a string getter is the would-be
+ * length excluding the NUL, so buf == NULL queries the size).
+ *
+ * - oak_color_transform: POD mirror of olive::ColorTransform. `output`
+ * is the colorspace name when `is_display` is 0, otherwise the display
+ * device name with `view`/`look` selecting the display transform. NULL
+ * strings mean "unset" (the empty QString).
+ *
+ * - OakEngineColorProcessor: owned handle wrapping an OCIO-backed
+ * olive::ColorProcessorPtr. Free with
+ * oakengine_color_processor_free(). Color conversion is per-color
+ * (double RGBA in/out); the frame-level GPU path goes through
+ * ColorTransformJob on the engine side.
+ *
+ * - OakEngineColorConfig: owned handle to a standalone OCIO config (the
+ * project properties dialog lists the colorspaces of a config file
+ * before applying it).
+ *
+ * Change notifications (config reloads, reference space changes) are
+ * delivered through the event family: subscribe with
+ * OAKENGINE_EVENT_COLOR_MANAGER_CONFIG_CHANGED /
+ * OAKENGINE_EVENT_COLOR_MANAGER_REFERENCE_SPACE_CHANGED (oakengine/events.h).
+ *
+ * Error model: functions that can hit an OCIO failure report the reason
+ * through oakengine_color_last_error() (thread-local, buf/size
+ * convention). List/query functions never fail on a valid handle.
+ */
+
+/** @brief Borrowed color manager handle (olive::ColorManager). */
+typedef struct OakEngineColorManager OakEngineColorManager;
+
+/** @brief Owned color processor handle; free with oakengine_color_processor_free(). */
+typedef struct OakEngineColorProcessor OakEngineColorProcessor;
+
+/** @brief Owned standalone OCIO config handle; free with oakengine_color_config_free(). */
+typedef struct OakEngineColorConfig OakEngineColorConfig;
+
+/** @brief Processor direction: input -> output (olive k_normal). */
+#define OAKENGINE_COLOR_PROCESSOR_NORMAL 0
+/** @brief Processor direction: output -> input (olive k_inverse). */
+#define OAKENGINE_COLOR_PROCESSOR_INVERSE 1
+
+/**
+ * @brief POD mirror of olive::ColorTransform. Strings are UTF-8; NULL is
+ * the unset/empty value.
+ */
+typedef struct oak_color_transform {
+ int is_display; /**< 0: `output` is a colorspace; 1: display/view/look. */
+ const char *output; /**< Colorspace name, or display device when is_display. */
+ const char *view; /**< Display view (is_display only). */
+ const char *look; /**< Display look (is_display only). */
+} oak_color_transform;
+
+/**
+ * @brief Human-readable reason of the last failed color call on this
+ * thread (buf/size convention). Empty when the last call succeeded.
+ */
+OAKENGINE_API int oakengine_color_last_error(char *buf, int buf_size);
+
+/**
+ * @brief The project's color manager (borrowed; NULL for a NULL project or
+ * a project without one).
+ */
+OAKENGINE_API OakEngineColorManager *
+oakengine_color_manager_from_project(OakEngineProject *project);
+
+/** @brief Current OCIO config filename of the manager (buf/size). */
+OAKENGINE_API int oakengine_color_manager_get_config_filename(
+ const OakEngineColorManager *mgr, char *buf, int buf_size);
+
+/**
+ * @brief Point the manager at a different OCIO config file
+ * (ColorManager::set_config_filename()). OAKENGINE_E_INVALID for NULL
+ * args. OCIO load failures surface lazily through the list queries.
+ */
+OAKENGINE_API int oakengine_color_manager_set_config_filename(
+ OakEngineColorManager *mgr, const char *filename);
+
+/** @brief Number of colorspaces in the manager's active config. */
+OAKENGINE_API int oakengine_color_manager_colorspace_count(
+ const OakEngineColorManager *mgr);
+
+/** @brief Name of the `index`-th colorspace (buf/size); OAKENGINE_E_INVALID out of range. */
+OAKENGINE_API int oakengine_color_manager_colorspace_at(
+ const OakEngineColorManager *mgr, int index, char *buf, int buf_size);
+
+/** @brief Number of display devices in the active config. */
+OAKENGINE_API int oakengine_color_manager_display_count(
+ const OakEngineColorManager *mgr);
+
+/** @brief Name of the `index`-th display device (buf/size). */
+OAKENGINE_API int oakengine_color_manager_display_at(
+ const OakEngineColorManager *mgr, int index, char *buf, int buf_size);
+
+/** @brief Number of views available on `display` (NULL/empty = active display). */
+OAKENGINE_API int oakengine_color_manager_view_count(
+ const OakEngineColorManager *mgr, const char *display);
+
+/** @brief Name of the `index`-th view on `display` (buf/size). */
+OAKENGINE_API int oakengine_color_manager_view_at(
+ const OakEngineColorManager *mgr, const char *display, int index,
+ char *buf, int buf_size);
+
+/** @brief Number of looks in the active config. */
+OAKENGINE_API int oakengine_color_manager_look_count(
+ const OakEngineColorManager *mgr);
+
+/** @brief Name of the `index`-th look (buf/size). */
+OAKENGINE_API int oakengine_color_manager_look_at(
+ const OakEngineColorManager *mgr, int index, char *buf, int buf_size);
+
+/** @brief The config's default display device (buf/size). */
+OAKENGINE_API int oakengine_color_manager_default_display(
+ const OakEngineColorManager *mgr, char *buf, int buf_size);
+
+/** @brief The config's default view for `display` (buf/size). */
+OAKENGINE_API int oakengine_color_manager_default_view(
+ const OakEngineColorManager *mgr, const char *display, char *buf,
+ int buf_size);
+
+/** @brief The project's default input colorspace (buf/size). */
+OAKENGINE_API int oakengine_color_manager_default_input_color_space(
+ const OakEngineColorManager *mgr, char *buf, int buf_size);
+
+/** @brief Set the project's default input colorspace. */
+OAKENGINE_API int oakengine_color_manager_set_default_input_color_space(
+ OakEngineColorManager *mgr, const char *colorspace);
+
+/** @brief The config's reference (scene-linear) colorspace (buf/size). */
+OAKENGINE_API int oakengine_color_manager_reference_color_space(
+ const OakEngineColorManager *mgr, char *buf, int buf_size);
+
+/**
+ * @brief The config's default luma coefficients written to `rgb` (exactly
+ * 3 doubles; ColorManager::get_default_luma_coefs()). OAKENGINE_E_INVALID
+ * for NULL args.
+ */
+OAKENGINE_API int oakengine_color_manager_default_luma_coefs(
+ const OakEngineColorManager *mgr, double *rgb);
+
+/**
+ * @brief Resolve `name` to a colorspace of the active config
+ * (ColorManager::get_compliant_color_space(QString); buf/size). Unknown
+ * names resolve to the default input colorspace.
+ */
+OAKENGINE_API int oakengine_color_manager_compliant_color_space(
+ const OakEngineColorManager *mgr, const char *name, char *buf,
+ int buf_size);
+
+/**
+ * @brief Resolve a transform to one the active config supports
+ * (ColorManager::get_compliant_color_space(ColorTransform, force_display)).
+ *
+ * The resolved transform is written into the output buffers; any output
+ * pointer may be NULL. Buffers that are too small truncate (NUL-terminated
+ * when size > 0). `out_is_display` receives the resolved kind.
+ *
+ * @return OAKENGINE_OK, or OAKENGINE_E_INVALID for NULL mgr/in.
+ */
+OAKENGINE_API int oakengine_color_manager_compliant_transform(
+ const OakEngineColorManager *mgr, const oak_color_transform *in,
+ int force_display, int *out_is_display, char *out_output,
+ int output_size, char *out_view, int view_size, char *out_look,
+ int look_size);
+
+/**
+ * @brief Load the engine's built-in default OCIO config (owned handle).
+ * NULL on failure (see oakengine_color_last_error()).
+ */
+OAKENGINE_API OakEngineColorConfig *oakengine_color_config_load_default(void);
+
+/**
+ * @brief Load an OCIO config from `filename` (owned handle). NULL on
+ * failure (see oakengine_color_last_error()).
+ */
+OAKENGINE_API OakEngineColorConfig *
+oakengine_color_config_load_file(const char *filename);
+
+/** @brief Release a config handle (NULL-safe no-op). */
+OAKENGINE_API void oakengine_color_config_free(OakEngineColorConfig *config);
+
+/** @brief Number of colorspaces in the config. */
+OAKENGINE_API int
+oakengine_color_config_colorspace_count(const OakEngineColorConfig *config);
+
+/** @brief Name of the `index`-th colorspace in the config (buf/size). */
+OAKENGINE_API int oakengine_color_config_colorspace_at(
+ const OakEngineColorConfig *config, int index, char *buf, int buf_size);
+
+/**
+ * @brief Create a color processor converting from colorspace `input` to
+ * the `dest` transform (ColorProcessor::create(); owned handle).
+ *
+ * `direction` is OAKENGINE_COLOR_PROCESSOR_NORMAL or
+ * OAKENGINE_COLOR_PROCESSOR_INVERSE. OCIO failures are non-fatal (matching
+ * the engine's C++ behavior): the handle is still returned but
+ * oakengine_color_processor_is_valid() reports 0 and conversions are
+ * pass-through.
+ *
+ * @return The handle, or NULL for NULL mgr/input/dest or an unknown
+ * direction.
+ */
+OAKENGINE_API OakEngineColorProcessor *oakengine_color_processor_create(
+ const OakEngineColorManager *mgr, const char *input,
+ const oak_color_transform *dest, int direction);
+
+/** @brief Release a processor handle (NULL-safe no-op). */
+OAKENGINE_API void oakengine_color_processor_free(OakEngineColorProcessor *proc);
+
+/**
+ * @brief 1 when the processor holds a valid OCIO processor
+ * (ColorProcessor::get_processor() != null), 0 otherwise.
+ */
+OAKENGINE_API int
+oakengine_color_processor_is_valid(const OakEngineColorProcessor *proc);
+
+/**
+ * @brief Convert a single RGBA color (ColorProcessor::convert_color()).
+ * `in_rgba`/`out_rgba` are 4-double arrays; on an invalid processor the
+ * input is copied through.
+ *
+ * @return OAKENGINE_OK, or OAKENGINE_E_INVALID for NULL args.
+ */
+OAKENGINE_API int oakengine_color_processor_convert_color(
+ const OakEngineColorProcessor *proc, const double *in_rgba,
+ double *out_rgba);
+
+/**
+ * @brief The OCIO cache id of the processor (ColorProcessor::id();
+ * buf/size). Used by display paths to invalidate cached conversions.
+ */
+OAKENGINE_API int oakengine_color_processor_id(
+ const OakEngineColorProcessor *proc, char *buf, int buf_size);
+
+/**
+ * @brief Attach a processor to an engine ColorTransformJob
+ * (ColorTransformJob::set_color_processor()).
+ *
+ * Transitional bridge for the display/scopes GPU path until the blit
+ * family covers ColorTransformJob: `job` is an
+ * olive::ColorTransformJob* the caller owns, passed as void* to keep the
+ * C++ type out of the ABI.
+ *
+ * @return OAKENGINE_OK, or OAKENGINE_E_INVALID for a NULL job.
+ */
+OAKENGINE_API int oakengine_color_transform_job_set_processor(
+ void *job, const OakEngineColorProcessor *proc);
+
+#ifdef __cplusplus
+}
+#endif
+
+#endif /* OAKENGINE_COLOR_H */
diff --git a/engine/include/oakengine/config.h b/engine/include/oakengine/config.h
new file mode 100644
index 000000000..db836e0e4
--- /dev/null
+++ b/engine/include/oakengine/config.h
@@ -0,0 +1,100 @@
+/***
+
+ 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 .
+
+***/
+
+#ifndef OAKENGINE_CONFIG_H
+#define OAKENGINE_CONFIG_H
+
+#include
+
+#include "init.h"
+
+#ifdef __cplusplus
+extern "C" {
+#endif
+
+/**
+ * @file config.h
+ * @brief C ABI for the engine configuration store (olive::Config).
+ *
+ * A thin facade over the QSettings-backed key/value store used by the editor
+ * for persistent preferences. Only the types actually used by the UI are
+ * exposed (string/int); the engine keeps ownership of the singleton.
+ */
+
+typedef void (*oakengine_config_error_fn)(const char *title,
+ const char *message,
+ void *userdata);
+
+/**
+ * @brief Load configuration from disk (Config::load).
+ */
+OAKENGINE_API int oakengine_config_load(void);
+
+/**
+ * @brief Save configuration to disk (Config::save).
+ */
+OAKENGINE_API int oakengine_config_save(void);
+
+/**
+ * @brief Read a string value (buf/size convention).
+ *
+ * @return the string length on success, 0 when the key is missing or empty,
+ * or a negative OAKENGINE_E_* code on error.
+ */
+OAKENGINE_API int oakengine_config_get_string(const char *key, char *buf,
+ int buf_size);
+
+/**
+ * @brief Write a string value.
+ */
+OAKENGINE_API int oakengine_config_set_string(const char *key,
+ const char *value);
+
+/**
+ * @brief Read an integer value. Returns `default_value` when the key is
+ * missing or not convertible to int.
+ */
+OAKENGINE_API int64_t oakengine_config_get_int(const char *key,
+ int64_t default_value);
+
+/**
+ * @brief Write an integer value.
+ */
+OAKENGINE_API int oakengine_config_set_int(const char *key, int64_t value);
+
+/**
+ * @brief Register a callback for configuration errors (e.g. disk write
+ * failures). Passing NULL clears the handler.
+ */
+OAKENGINE_API int oakengine_config_set_error_handler(
+ oakengine_config_error_fn fn, void *userdata);
+
+/**
+ * @brief Report an error through the registered handler. If no handler is
+ * set the error is logged and discarded.
+ */
+OAKENGINE_API int oakengine_config_report_error(const char *title,
+ const char *message);
+
+#ifdef __cplusplus
+}
+#endif
+
+#endif /* OAKENGINE_CONFIG_H */
diff --git a/engine/include/oakengine/disk.h b/engine/include/oakengine/disk.h
new file mode 100644
index 000000000..d4bf64268
--- /dev/null
+++ b/engine/include/oakengine/disk.h
@@ -0,0 +1,154 @@
+/***
+
+ 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 .
+
+***/
+
+#ifndef OAKENGINE_DISK_H
+#define OAKENGINE_DISK_H
+
+#include "export.h"
+#include "init.h"
+#include "project.h"
+
+#ifdef __cplusplus
+extern "C" {
+#endif
+
+/**
+ * @file disk.h
+ * @brief C ABI for the engine's disk cache singleton (olive::DiskManager)
+ *
+ * A thin facade over DiskManager's instance lifecycle, default/custom cache
+ * path management, cache clearing, settings dialog dispatch and project
+ * invalidation. The opaque folder handle returned by
+ * oakengine_disk_get_open_folder() is a borrowed pointer to the engine's
+ * internal DiskCacheFolder for that path; it must not be freed and becomes
+ * invalid when the DiskManager instance is destroyed.
+ *
+ * Conventions match the other facade families:
+ * - 0 (OAKENGINE_OK) / negative OAKENGINE_E_* codes.
+ * - String output uses the buf/size convention.
+ * - Booleans are int (1/0).
+ */
+
+/**
+ * @brief Callback invoked when the engine requests the disk cache settings
+ * dialog for a folder.
+ *
+ * `folder_path` is the UTF-8 path of the cache folder. `parent_window` is a
+ * borrowed pointer to the QWidget that should act as the dialog's parent (may
+ * be NULL). `userdata` is the value passed to
+ * oakengine_disk_set_settings_handler().
+ */
+typedef void (*oakengine_disk_settings_fn)(const char *folder_path,
+ void *parent_window,
+ void *userdata);
+
+/**
+ * @brief Create the DiskManager singleton.
+ *
+ * Safe to call when the instance already exists (no-op). Returns
+ * OAKENGINE_OK or OAKENGINE_E_FAILED.
+ */
+OAKENGINE_API int oakengine_disk_create_instance(void);
+
+/**
+ * @brief Destroy the DiskManager singleton.
+ *
+ * Safe to call when no instance exists (no-op). Returns OAKENGINE_OK.
+ */
+OAKENGINE_API int oakengine_disk_destroy_instance(void);
+
+/**
+ * @brief Register the handler used to show the disk cache settings dialog.
+ *
+ * The engine calls this handler when the user requests the settings dialog.
+ * Passing NULL clears the handler. Returns OAKENGINE_OK.
+ */
+OAKENGINE_API int oakengine_disk_set_settings_handler(
+ oakengine_disk_settings_fn fn, void *userdata);
+
+/**
+ * @brief Show the disk cache settings dialog for `path`.
+ *
+ * If `path` is NULL or empty, the default cache folder is used. The actual
+ * dialog is shown by the handler registered with
+ * oakengine_disk_set_settings_handler(); if no handler is registered the
+ * request is logged and skipped. Returns OAKENGINE_OK or an error code.
+ */
+OAKENGINE_API int oakengine_disk_show_settings_dialog(const char *path,
+ void *parent_window);
+
+/**
+ * @brief Show a confirmation dialog before changing the disk cache location.
+ *
+ * Returns 1 if the user confirms, 0 otherwise. `parent_window` may be NULL.
+ */
+OAKENGINE_API int oakengine_disk_show_change_confirmation_dialog(
+ void *parent_window);
+
+/**
+ * @brief Clear the disk cache in `path`.
+ *
+ * Returns 1 on success, 0 on failure. The folder is opened if necessary.
+ */
+OAKENGINE_API int oakengine_disk_clear_cache(const char *path);
+
+/**
+ * @brief Get the default cache folder path (buf/size convention).
+ *
+ * Returns the string length on success, or a negative OAKENGINE_E_* code when
+ * no DiskManager instance exists.
+ */
+OAKENGINE_API int oakengine_disk_get_default_cache_path(char *buf,
+ int buf_size);
+
+/**
+ * @brief Set the default cache folder path.
+ *
+ * The default folder's path is updated and will be persisted when the
+ * DiskManager instance is destroyed. Returns OAKENGINE_OK or an error code.
+ */
+OAKENGINE_API int oakengine_disk_set_default_cache_path(const char *path);
+
+/**
+ * @brief Get or create a borrowed opaque handle to the cache folder for
+ * `path`.
+ *
+ * Returns NULL if no DiskManager instance exists or if `path` is invalid. If
+ * `path` is NULL or empty, the default cache folder is returned. The returned
+ * handle is a borrowed pointer whose lifetime follows the DiskManager
+ * instance; it must not be freed.
+ */
+OAKENGINE_API void *oakengine_disk_get_open_folder(const char *path);
+
+/**
+ * @brief Emit the invalidate_project signal on the DiskManager instance.
+ *
+ * This tells consumers of the disk cache that `project` has changed and any
+ * cached data for it should be discarded. Returns OAKENGINE_OK or an error
+ * code.
+ */
+OAKENGINE_API int oakengine_disk_invalidate_project(
+ OakEngineProject *project);
+
+#ifdef __cplusplus
+}
+#endif
+
+#endif /* OAKENGINE_DISK_H */
diff --git a/engine/include/oakengine/display.h b/engine/include/oakengine/display.h
new file mode 100644
index 000000000..1a58b8e32
--- /dev/null
+++ b/engine/include/oakengine/display.h
@@ -0,0 +1,186 @@
+/***
+
+ 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 .
+
+***/
+
+#ifndef OAKENGINE_DISPLAY_H
+#define OAKENGINE_DISPLAY_H
+
+#include "export.h"
+#include "init.h"
+
+#ifdef __cplusplus
+extern "C" {
+#endif
+
+/**
+ * @file display.h
+ * @brief C ABI for the GPU display renderer used by viewer/scope widgets
+ *
+ * This family wraps the engine's interactive display renderer
+ * (olive::Renderer and its OpenGLRenderer/DynamicRenderer implementations,
+ * engine/render/renderer.h) plus the GPU texture (olive::Texture) and the
+ * CPU frame buffer (olive::Frame) that viewer/scope widgets use to move
+ * pixels between the CPU and the GPU.
+ *
+ * It is distinct from the sequence-rendering facade in oakengine/renderer.h
+ * (OakEngineRenderer), which pulls finished CPU frames out of the async
+ * render pipeline. This family drives the *on-screen* paint path instead:
+ * a widget creates a renderer, initializes it with the widget's GL context,
+ * uploads/downloads textures, and blits color-managed images each paint.
+ *
+ * Conventions (matching the other facade families):
+ * - All object pointers are opaque. `renderer` is an olive::Renderer*,
+ * `texture` an olive::Texture*, `frame` an olive::Frame*.
+ * - `out_texture` / `out_frame` are pointers to caller-owned
+ * olive::TexturePtr / olive::FramePtr (std::shared_ptr) storage; the
+ * callee assigns a newly created smart pointer into them, releasing any
+ * previously held object. This keeps shared-pointer ownership/deleter
+ * bookkeeping entirely on the engine side.
+ * - `video_params` is a `const olive::VideoParams*`; `color_job` is a
+ * `const olive::ColorTransformJob*`. These are passed as opaque pointers
+ * because they are C++ types; both the caller (app) and the callee
+ * (engine) are compiled as C++ against the same headers.
+ * - `gl_context` is a `QOpenGLContext*` or NULL.
+ * - `parent` is the owning `QObject*` (the display widget); the created
+ * renderer is a QObject child of it and is destroyed by Qt ownership.
+ * Do NOT call oakengine_display_renderer_destroy() and then also rely on
+ * Qt deletion of the same renderer's GPU resources -- destroy() releases
+ * GPU state, Qt deletion releases the object.
+ */
+
+/* ---- Display renderer lifecycle ---------------------------------------- */
+
+/**
+ * @brief Create a dynamic-backend renderer (olive::DynamicRenderer) for
+ * `backend_name` and load() it.
+ *
+ * @return The renderer (olive::Renderer*), or NULL if the backend library
+ * could not be loaded (the failed renderer is deleted internally and
+ * the caller should fall back to
+ * oakengine_display_renderer_create_opengl()). NULL is also returned
+ * when the engine was built without dynamic-backend support.
+ */
+OAKENGINE_API void *
+oakengine_display_renderer_create_dynamic(const char *backend_name,
+ void *parent);
+
+/**
+ * @brief Create the built-in OpenGL renderer (olive::OpenGLRenderer).
+ *
+ * @return The renderer (olive::Renderer*), never NULL.
+ */
+OAKENGINE_API void *oakengine_display_renderer_create_opengl(void *parent);
+
+/**
+ * @brief Initialize a display renderer and run its post-init step.
+ *
+ * If `gl_context` is non-NULL the OpenGL/dynamic path is taken (the renderer
+ * is initialized against the widget's shared QOpenGLContext); otherwise the
+ * backend-neutral path (Renderer::init()/post_init()) is used.
+ *
+ * @return OAKENGINE_OK on success, OAKENGINE_E_INVALID for a NULL renderer.
+ */
+OAKENGINE_API int oakengine_display_renderer_init(void *renderer,
+ void *gl_context);
+
+/**
+ * @brief Release a display renderer's GPU resources (Renderer::destroy()
+ * followed by post_destroy()). The renderer object itself remains owned by
+ * its Qt parent.
+ */
+OAKENGINE_API void oakengine_display_renderer_destroy(void *renderer);
+
+/* ---- Texture creation and pixel transfer -------------------------------- */
+
+/**
+ * @brief Create a GPU texture on `renderer` (Renderer::create_texture()).
+ *
+ * @param renderer olive::Renderer*.
+ * @param video_params const olive::VideoParams* describing the texture.
+ * @param pixels Initial pixel data, or NULL for an empty texture.
+ * @param linesize Line stride of `pixels` (ignored when NULL).
+ * @param out_texture Pointer to an olive::TexturePtr to receive the result.
+ */
+OAKENGINE_API void
+oakengine_display_renderer_create_texture(void *renderer,
+ const void *video_params,
+ const void *pixels, int linesize,
+ void *out_texture);
+
+/**
+ * @brief Blit a color-managed image (Renderer::blit_color_managed()).
+ *
+ * @param renderer olive::Renderer*.
+ * @param color_job const olive::ColorTransformJob*.
+ * @param dst_texture Destination olive::Texture*, or NULL to blit to the
+ * current output destination.
+ * @param video_params const olive::VideoParams* for the destination, or NULL
+ * to use dst_texture's own parameters (in which case
+ * dst_texture must be non-NULL).
+ */
+OAKENGINE_API void
+oakengine_display_renderer_blit_color_managed(void *renderer,
+ const void *color_job,
+ void *dst_texture,
+ const void *video_params);
+
+/**
+ * @brief Upload CPU pixels into a GPU texture (Texture::upload()).
+ */
+OAKENGINE_API void oakengine_display_texture_upload(void *texture,
+ void *pixels, int linesize);
+
+/**
+ * @brief Download GPU texture pixels into CPU memory (Texture::download()).
+ */
+OAKENGINE_API void oakengine_display_texture_download(void *texture,
+ void *pixels,
+ int linesize);
+
+/* ---- CPU frame buffer --------------------------------------------------- */
+
+/**
+ * @brief Create an empty CPU frame (olive::Frame::create()).
+ *
+ * @param out_frame Pointer to an olive::FramePtr to receive the new frame.
+ */
+OAKENGINE_API void oakengine_codec_frame_create(void *out_frame);
+
+/**
+ * @brief Set a frame's video parameters (Frame::set_video_params()).
+ *
+ * @param frame olive::Frame*.
+ * @param video_params const olive::VideoParams*.
+ */
+OAKENGINE_API void oakengine_codec_frame_set_video_params(void *frame,
+ const void
+ *video_params);
+
+/**
+ * @brief Allocate the frame's pixel buffer (Frame::allocate()).
+ *
+ * @return 1 on success, 0 on failure or NULL frame.
+ */
+OAKENGINE_API int oakengine_codec_frame_allocate(void *frame);
+
+#ifdef __cplusplus
+}
+#endif
+
+#endif /* OAKENGINE_DISPLAY_H */
diff --git a/engine/include/oakengine/encoding.h b/engine/include/oakengine/encoding.h
new file mode 100644
index 000000000..1972867a4
--- /dev/null
+++ b/engine/include/oakengine/encoding.h
@@ -0,0 +1,505 @@
+/***
+
+ 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 .
+
+***/
+
+#ifndef OAKENGINE_ENCODING_H
+#define OAKENGINE_ENCODING_H
+
+#include
+
+#include "export.h"
+#include "timeline.h"
+#include "videoparams.h"
+
+#ifdef __cplusplus
+extern "C" {
+#endif
+
+/**
+ * @file encoding.h
+ * @brief C ABI for the encoding parameter surface (EncodingParams /
+ * ExportFormat / ExportCodec)
+ *
+ * This family exposes everything the application's export dialog (and the
+ * audio-recording path) needs without touching the engine's C++ classes:
+ *
+ * - Container/codec metadata queries (format names/extensions, codec lists
+ * per format, codec names/flags, supported pixel and sample formats).
+ * - An opaque OakEngineEncodingParams handle wrapping the engine's
+ * EncodingParams: full getter/setter surface, preset path/listing and
+ * preset load/save.
+ * - oakengine_export_render_with_params(): runs the same synchronous export
+ * path as oakengine_export_render_ex() (oakengine/exporter.h) using a
+ * params handle assembled through this family.
+ *
+ * Enum int fields carry the engine's own enum values
+ * (olive::ExportFormat::Format, olive::ExportCodec::Codec,
+ * olive::VideoParams::Interlacing/ColorRange, olive::PixelFormat::Format,
+ * olive::core::SampleFormat::Format). Conventions match the other facade
+ * families: 0 (OAKENGINE_OK) / negative OAKENGINE_E_* codes, buf/size
+ * strings (return value is the would-be length excluding the NUL), NULL
+ * handles are no-ops returning the documented failure value.
+ */
+
+/** @brief Opaque encoding-parameters handle (olive::EncodingParams). */
+typedef struct OakEngineEncodingParams OakEngineEncodingParams;
+
+/** @brief Scaling method values (EncodingParams::VideoScalingMethod). */
+#define OAKENGINE_ENCODING_SCALING_FIT 0
+#define OAKENGINE_ENCODING_SCALING_STRETCH 1
+#define OAKENGINE_ENCODING_SCALING_CROP 2
+
+/**
+ * @brief Container formats (olive::ExportFormat::Format) referenced by name
+ * in UI code. Only append; the values are serialized in project/preset
+ * files. The complete list lives in engine/codec/exportformat.h.
+ */
+#define OAKENGINE_ENCODING_FORMAT_MATROSKA 1
+#define OAKENGINE_ENCODING_FORMAT_MPEG4_VIDEO 2
+#define OAKENGINE_ENCODING_FORMAT_QUICKTIME 4
+#define OAKENGINE_ENCODING_FORMAT_PNG 5
+#define OAKENGINE_ENCODING_FORMAT_WAV 7
+#define OAKENGINE_ENCODING_FORMAT_SRT 13
+
+/**
+ * @brief Codecs (olive::ExportCodec::Codec) referenced by name in UI code.
+ * Only append; the values are serialized. The complete list lives in
+ * engine/codec/exportcodec.h.
+ */
+#define OAKENGINE_ENCODING_CODEC_H264 1
+#define OAKENGINE_ENCODING_CODEC_H264RGB 2
+#define OAKENGINE_ENCODING_CODEC_H265 3
+#define OAKENGINE_ENCODING_CODEC_CINEFORM 7
+#define OAKENGINE_ENCODING_CODEC_AAC 12
+#define OAKENGINE_ENCODING_CODEC_PCM 13
+#define OAKENGINE_ENCODING_CODEC_SRT 17
+#define OAKENGINE_ENCODING_CODEC_AV1 18
+
+/** @brief olive::VideoParams::ColorRange values. */
+#define OAKENGINE_ENCODING_COLOR_RANGE_LIMITED 0
+#define OAKENGINE_ENCODING_COLOR_RANGE_FULL 1
+
+/** @brief olive::VideoParams::Interlacing values. */
+#define OAKENGINE_ENCODING_INTERLACE_NONE 0
+#define OAKENGINE_ENCODING_INTERLACE_TOP_FIRST 1
+#define OAKENGINE_ENCODING_INTERLACE_BOTTOM_FIRST 2
+
+/* ---- Container format / codec metadata ---------------------------------- */
+
+/** @brief Number of container formats (olive::ExportFormat::k_format_count). */
+OAKENGINE_API int oakengine_encoding_format_count(void);
+
+/** @brief Display name of a container format (buf/size); -1 invalid. */
+OAKENGINE_API int oakengine_encoding_format_name(int format, char *buf,
+ int buf_size);
+
+/** @brief File extension (no dot) of a container format (buf/size). */
+OAKENGINE_API int oakengine_encoding_format_extension(int format, char *buf,
+ int buf_size);
+
+/**
+ * @brief Number of video codecs a container format supports; -1 when the
+ * format is invalid.
+ */
+OAKENGINE_API int oakengine_encoding_format_video_codec_count(int format);
+
+/**
+ * @brief The `index`-th video codec of `format` as an
+ * olive::ExportCodec::Codec value; -1 when out of range.
+ */
+OAKENGINE_API int oakengine_encoding_format_video_codec_at(int format,
+ int index);
+
+/** @brief Audio-codec variant of the two functions above. */
+OAKENGINE_API int oakengine_encoding_format_audio_codec_count(int format);
+OAKENGINE_API int oakengine_encoding_format_audio_codec_at(int format,
+ int index);
+
+/** @brief Subtitle-codec variant of the two functions above. */
+OAKENGINE_API int oakengine_encoding_format_subtitle_codec_count(int format);
+OAKENGINE_API int oakengine_encoding_format_subtitle_codec_at(int format,
+ int index);
+
+/** @brief Display name of a codec (buf/size); -1 when invalid. */
+OAKENGINE_API int oakengine_encoding_codec_name(int codec, char *buf,
+ int buf_size);
+
+/** @brief 1 when `codec` encodes still images (PNG/TIFF/OpenEXR). */
+OAKENGINE_API int oakengine_encoding_codec_is_still_image(int codec);
+
+/** @brief 1 when `codec` is lossless (no bit-rate setting applies). */
+OAKENGINE_API int oakengine_encoding_codec_is_lossless(int codec);
+
+/**
+ * @brief Number of encoded pixel formats (e.g. "yuv420p") usable with
+ * `codec` inside `format`; -1 when invalid.
+ */
+OAKENGINE_API int oakengine_encoding_pix_fmt_count(int format, int codec);
+
+/** @brief The `index`-th encoded pixel format name (buf/size). */
+OAKENGINE_API int oakengine_encoding_pix_fmt_at(int format, int codec,
+ int index, char *buf,
+ int buf_size);
+
+/**
+ * @brief Index of `pix_fmt` (e.g. "yuv420p") in `codec`'s supported pixel
+ * format list; 0 (the codec's preferred format) when absent or `pix_fmt` is
+ * NULL/empty.
+ */
+OAKENGINE_API int oakengine_encoding_pix_fmt_index(int codec,
+ const char *pix_fmt);
+
+/**
+ * @brief Number of sample formats usable with `codec` inside `format`;
+ * -1 when invalid.
+ */
+OAKENGINE_API int oakengine_encoding_sample_format_count(int format,
+ int codec);
+
+/**
+ * @brief The `index`-th sample format as an olive::core::SampleFormat::Format
+ * value; -1 when out of range.
+ */
+OAKENGINE_API int oakengine_encoding_sample_format_at(int format, int codec,
+ int index);
+
+/* ---- Image-sequence filename helpers (olive::Encoder statics) ----------- */
+
+/** @brief 1 when `filename` contains a "[#####]" digit placeholder. */
+OAKENGINE_API int
+oakengine_encoding_filename_contains_digit_placeholder(const char *filename);
+
+/**
+ * @brief Digit count of the filename's "[#####]" placeholder; 0 when none.
+ */
+OAKENGINE_API int
+oakengine_encoding_image_sequence_digit_count(const char *filename);
+
+/** @brief `filename` with the digit placeholder removed (buf/size). */
+OAKENGINE_API int
+oakengine_encoding_filename_remove_digit_placeholder(const char *filename,
+ char *buf, int buf_size);
+
+/**
+ * @brief Fit/stretch/crop transform matrix
+ * (EncodingParams::generate_matrix()).
+ *
+ * Writes the 16 floats of the column-major 4x4 matrix to `out16`
+ * (QMatrix4x4 layout). `method` is OAKENGINE_ENCODING_SCALING_*.
+ *
+ * @return OAKENGINE_OK, or OAKENGINE_E_INVALID for bad arguments.
+ */
+OAKENGINE_API int oakengine_encoding_generate_matrix(int method, int src_width,
+ int src_height,
+ int dest_width,
+ int dest_height,
+ float out16[16]);
+
+/* ---- Encoding parameters handle ----------------------------------------- */
+
+/**
+ * @brief Create an empty encoding-parameters handle (all tracks disabled,
+ * format unset). Destroy with oakengine_encoding_params_destroy().
+ */
+OAKENGINE_API OakEngineEncodingParams *oakengine_encoding_params_create(void);
+
+/** @brief Destroy a handle created by oakengine_encoding_params_create(). */
+OAKENGINE_API void
+oakengine_encoding_params_destroy(OakEngineEncodingParams *params);
+
+/**
+ * @brief 1 when at least one of video/audio/subtitles is enabled
+ * (EncodingParams::is_valid()).
+ */
+OAKENGINE_API int
+oakengine_encoding_params_is_valid(const OakEngineEncodingParams *params);
+
+/** @brief Output filename (buf/size convention). */
+OAKENGINE_API int
+oakengine_encoding_params_set_filename(OakEngineEncodingParams *params,
+ const char *filename);
+OAKENGINE_API int
+oakengine_encoding_params_filename(const OakEngineEncodingParams *params,
+ char *buf, int buf_size);
+
+/**
+ * @brief Container format as olive::ExportFormat::Format; the getter returns
+ * -1 when unset. The setter rejects out-of-range values with
+ * OAKENGINE_E_INVALID.
+ */
+OAKENGINE_API int
+oakengine_encoding_params_set_format(OakEngineEncodingParams *params,
+ int format);
+OAKENGINE_API int
+oakengine_encoding_params_format(const OakEngineEncodingParams *params);
+
+/**
+ * @brief Enable video with the given parameters and codec
+ * (EncodingParams::enable_video()).
+ */
+OAKENGINE_API int
+oakengine_encoding_params_enable_video(OakEngineEncodingParams *params,
+ const oak_video_params *video,
+ int codec);
+
+/**
+ * @brief Enable audio (EncodingParams::enable_audio()). `sample_format` is
+ * an olive::core::SampleFormat::Format value.
+ */
+OAKENGINE_API int
+oakengine_encoding_params_enable_audio(OakEngineEncodingParams *params,
+ int sample_rate,
+ uint64_t channel_layout,
+ int sample_format, int codec);
+
+/** @brief Enable embedded subtitles. */
+OAKENGINE_API int
+oakengine_encoding_params_enable_subtitles(OakEngineEncodingParams *params,
+ int codec);
+
+/** @brief Enable sidecar subtitles with the given sidecar container. */
+OAKENGINE_API int oakengine_encoding_params_enable_sidecar_subtitles(
+ OakEngineEncodingParams *params, int format, int codec);
+
+OAKENGINE_API void
+oakengine_encoding_params_disable_video(OakEngineEncodingParams *params);
+OAKENGINE_API void
+oakengine_encoding_params_disable_audio(OakEngineEncodingParams *params);
+OAKENGINE_API void
+oakengine_encoding_params_disable_subtitles(OakEngineEncodingParams *params);
+
+OAKENGINE_API int
+oakengine_encoding_params_video_enabled(const OakEngineEncodingParams *params);
+OAKENGINE_API int
+oakengine_encoding_params_video_codec(const OakEngineEncodingParams *params);
+
+/**
+ * @brief Read back the video parameters (any field may be NULL);
+ * OAKENGINE_E_STATE when video is disabled.
+ */
+OAKENGINE_API int oakengine_encoding_params_get_video_params(
+ const OakEngineEncodingParams *params, oak_video_params *out);
+
+OAKENGINE_API int
+oakengine_encoding_params_audio_enabled(const OakEngineEncodingParams *params);
+OAKENGINE_API int
+oakengine_encoding_params_audio_codec(const OakEngineEncodingParams *params);
+
+/**
+ * @brief Read back the audio parameters (any field may be NULL);
+ * OAKENGINE_E_STATE when audio is disabled.
+ */
+OAKENGINE_API int oakengine_encoding_params_get_audio_params(
+ const OakEngineEncodingParams *params, int *sample_rate,
+ uint64_t *channel_layout, int *sample_format);
+
+OAKENGINE_API int oakengine_encoding_params_subtitles_enabled(
+ const OakEngineEncodingParams *params);
+OAKENGINE_API int oakengine_encoding_params_subtitles_are_sidecar(
+ const OakEngineEncodingParams *params);
+OAKENGINE_API int oakengine_encoding_params_subtitles_sidecar_format(
+ const OakEngineEncodingParams *params);
+OAKENGINE_API int oakengine_encoding_params_subtitles_codec(
+ const OakEngineEncodingParams *params);
+
+/** @brief Video bit rates / buffer size (bit/s, bytes). */
+OAKENGINE_API void
+oakengine_encoding_params_set_video_bit_rate(OakEngineEncodingParams *params,
+ int64_t rate);
+OAKENGINE_API int64_t
+oakengine_encoding_params_video_bit_rate(const OakEngineEncodingParams *params);
+OAKENGINE_API void
+oakengine_encoding_params_set_video_min_bit_rate(
+ OakEngineEncodingParams *params, int64_t rate);
+OAKENGINE_API int64_t oakengine_encoding_params_video_min_bit_rate(
+ const OakEngineEncodingParams *params);
+OAKENGINE_API void
+oakengine_encoding_params_set_video_max_bit_rate(
+ OakEngineEncodingParams *params, int64_t rate);
+OAKENGINE_API int64_t oakengine_encoding_params_video_max_bit_rate(
+ const OakEngineEncodingParams *params);
+OAKENGINE_API void
+oakengine_encoding_params_set_video_buffer_size(
+ OakEngineEncodingParams *params, int64_t size);
+OAKENGINE_API int64_t oakengine_encoding_params_video_buffer_size(
+ const OakEngineEncodingParams *params);
+
+/** @brief Encoder thread count (0 = auto). */
+OAKENGINE_API void
+oakengine_encoding_params_set_video_threads(OakEngineEncodingParams *params,
+ int threads);
+OAKENGINE_API int
+oakengine_encoding_params_video_threads(const OakEngineEncodingParams *params);
+
+/** @brief Audio bit rate (bit/s). */
+OAKENGINE_API void
+oakengine_encoding_params_set_audio_bit_rate(OakEngineEncodingParams *params,
+ int64_t rate);
+OAKENGINE_API int64_t
+oakengine_encoding_params_audio_bit_rate(const OakEngineEncodingParams *params);
+
+/** @brief Encoded pixel format name (e.g. "yuv420p"; buf/size getter). */
+OAKENGINE_API int
+oakengine_encoding_params_set_video_pix_fmt(OakEngineEncodingParams *params,
+ const char *pix_fmt);
+OAKENGINE_API int
+oakengine_encoding_params_video_pix_fmt(
+ const OakEngineEncodingParams *params, char *buf, int buf_size);
+
+/** @brief Image-sequence flag (0/1). */
+OAKENGINE_API void
+oakengine_encoding_params_set_video_is_image_sequence(
+ OakEngineEncodingParams *params, int is_image_sequence);
+OAKENGINE_API int oakengine_encoding_params_video_is_image_sequence(
+ const OakEngineEncodingParams *params);
+
+/**
+ * @brief Output color transform by OCIO color space name; an empty/NULL
+ * name selects the reference space (no transform).
+ */
+OAKENGINE_API int oakengine_encoding_params_set_color_transform(
+ OakEngineEncodingParams *params, const char *output_name);
+OAKENGINE_API int oakengine_encoding_params_color_transform_output(
+ const OakEngineEncodingParams *params, char *buf, int buf_size);
+
+/** @brief Export length as rational seconds. */
+OAKENGINE_API void
+oakengine_encoding_params_set_export_length(OakEngineEncodingParams *params,
+ int num, int den);
+OAKENGINE_API int
+oakengine_encoding_params_get_export_length(
+ const OakEngineEncodingParams *params, int *num, int *den);
+
+/**
+ * @brief Custom export range as rational seconds [in, out). The getter
+ * returns OAKENGINE_E_NOT_FOUND when no custom range is set.
+ */
+OAKENGINE_API void
+oakengine_encoding_params_set_custom_range(OakEngineEncodingParams *params,
+ int64_t in_num, int64_t in_den,
+ int64_t out_num, int64_t out_den);
+OAKENGINE_API int
+oakengine_encoding_params_has_custom_range(
+ const OakEngineEncodingParams *params);
+OAKENGINE_API int
+oakengine_encoding_params_get_custom_range(
+ const OakEngineEncodingParams *params, int64_t *in_num, int64_t *in_den,
+ int64_t *out_num, int64_t *out_den);
+
+/** @brief Scaling method (OAKENGINE_ENCODING_SCALING_*). */
+OAKENGINE_API int
+oakengine_encoding_params_set_video_scaling_method(
+ OakEngineEncodingParams *params, int method);
+OAKENGINE_API int oakengine_encoding_params_video_scaling_method(
+ const OakEngineEncodingParams *params);
+
+/**
+ * @brief Encoder-specific video option (key/value strings, e.g. "crf" =
+ * "18"); mirrors EncodingParams::set_video_option(). The getter returns the
+ * would-be length (buf/size) or OAKENGINE_E_NOT_FOUND when the key is unset.
+ */
+OAKENGINE_API int
+oakengine_encoding_params_set_video_option(OakEngineEncodingParams *params,
+ const char *key, const char *value);
+OAKENGINE_API int
+oakengine_encoding_params_video_option(const OakEngineEncodingParams *params,
+ const char *key, char *buf,
+ int buf_size);
+
+/* ---- Presets ------------------------------------------------------------- */
+
+/** @brief Directory where export presets live (buf/size). */
+OAKENGINE_API int oakengine_encoding_preset_path(char *buf, int buf_size);
+
+/** @brief Number of saved presets. */
+OAKENGINE_API int oakengine_encoding_preset_count(void);
+
+/** @brief Name of the `index`-th preset (buf/size); -1 when out of range. */
+OAKENGINE_API int oakengine_encoding_preset_name(int index, char *buf,
+ int buf_size);
+
+/**
+ * @brief Load parameters from a preset/XML file (overwrites the handle's
+ * contents on success).
+ *
+ * @return OAKENGINE_OK, OAKENGINE_E_INVALID for bad arguments, or
+ * OAKENGINE_E_FAILED when the file cannot be read or parsed.
+ */
+OAKENGINE_API int
+oakengine_encoding_params_load_file(OakEngineEncodingParams *params,
+ const char *path);
+
+/** @brief Save parameters to a preset/XML file (same return convention). */
+OAKENGINE_API int
+oakengine_encoding_params_save_file(const OakEngineEncodingParams *params,
+ const char *path);
+
+/* ---- Export execution / per-sequence last-used --------------------------- */
+
+/**
+ * @brief Run a synchronous offline export with a params handle assembled
+ * through this family.
+ *
+ * Same blocking/progress/cancel semantics as oakengine_export_render_ex()
+ * (oakengine/exporter.h): progress via
+ * oakengine_export_set_progress_callback(), cancellation via
+ * oakengine_export_cancel(), failure reason via
+ * oakengine_export_last_error(). The output filename and image-sequence
+ * template come from the handle itself.
+ *
+ * @return OAKENGINE_OK / OAKENGINE_E_INVALID / OAKENGINE_E_STATE /
+ * OAKENGINE_E_FAILED / OAKENGINE_E_CANCELLED.
+ */
+OAKENGINE_API int
+oakengine_export_render_with_params(OakEngineSequence *seq,
+ const OakEngineEncodingParams *params);
+
+/**
+ * @brief Copy of the sequence's last-used encoding parameters
+ * (ViewerOutput::get_last_used_encoding_params()), or NULL when none is
+ * valid. Caller destroys with oakengine_encoding_params_destroy().
+ */
+OAKENGINE_API OakEngineEncodingParams *
+oakengine_encoding_params_get_last_used(OakEngineSequence *seq);
+
+/**
+ * @brief Store `params` as the sequence's last-used encoding parameters
+ * (ViewerOutput::set_last_used_encoding_params()); NULL is a no-op.
+ */
+OAKENGINE_API void oakengine_encoding_params_set_last_used(
+ OakEngineSequence *seq, const OakEngineEncodingParams *params);
+
+/**
+ * @brief Start audio recording to the file described by `params`
+ * (AudioManager::start_recording(); audio must be enabled on the handle).
+ *
+ * @return OAKENGINE_OK on success; OAKENGINE_E_INVALID for bad arguments;
+ * OAKENGINE_E_STATE when the audio manager is not running;
+ * OAKENGINE_E_FAILED otherwise (a human-readable reason is written to
+ * `errbuf`/`errbuf_size` when given).
+ */
+OAKENGINE_API int
+oakengine_encoding_start_audio_recording(const OakEngineEncodingParams *params,
+ char *errbuf, int errbuf_size);
+
+#ifdef __cplusplus
+}
+#endif
+
+#endif /* OAKENGINE_ENCODING_H */
diff --git a/engine/include/oakengine/events.h b/engine/include/oakengine/events.h
new file mode 100644
index 000000000..2154b1ba3
--- /dev/null
+++ b/engine/include/oakengine/events.h
@@ -0,0 +1,263 @@
+/***
+
+ 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 .
+
+***/
+
+#ifndef OAKENGINE_EVENTS_H
+#define OAKENGINE_EVENTS_H
+
+#include
+
+#include "export.h"
+#include "init.h"
+#include "project.h"
+#include "timeline.h"
+
+#ifdef __cplusplus
+extern "C" {
+#endif
+
+/**
+ * @file events.h
+ * @brief C ABI for engine change notifications (the signal/slot replacement)
+ *
+ * The engine's C++ API notifies observers through Qt signals (Project::
+ * modified_changed, Folder::begin_insert_item, Track::block_added, the
+ * sequence's track/marker/workarea notifications, ...). This family exposes
+ * the same notifications to C consumers as a subscription/callback
+ * mechanism, so the application never needs to connect() to an engine
+ * QObject directly.
+ *
+ * Usage:
+ *
+ * int64_t sub = oakengine_event_subscribe(handle, OAKENGINE_EVENT_..., fn,
+ * userdata);
+ * ...
+ * oakengine_event_unsubscribe(sub);
+ *
+ * `handle` is a borrowed facade handle whose static type depends on the
+ * event family (see the table below); a mismatch or NULL handle fails with
+ * 0 (an invalid subscription id). Subscribing the same (handle, event)
+ * twice is allowed and returns two independent subscription ids.
+ *
+ * Thread semantics: callbacks are invoked SYNCHRONOUSLY on the thread that
+ * emits the change (the equivalent of Qt::DirectConnection) before the
+ * engine's own emission returns, exactly like the C++ connections they
+ * replace. The callback runs under whatever locks the engine holds at the
+ * emission site; it must not call back into editing primitives that mutate
+ * the same object. All engine objects live on the GUI thread, so callbacks
+ * normally fire there.
+ *
+ * Lifetime: the registry drops the subscription automatically when the
+ * observed engine object is destroyed, so a stale subscription id is never
+ * a use-after-free; oakengine_event_unsubscribe() on an id whose object
+ * died is a harmless no-op returning OAKENGINE_E_NOT_FOUND. The inverse is
+ * NOT tracked: `userdata` ownership stays with the subscriber, which must
+ * unsubscribe (or tolerate callbacks) until its own teardown.
+ *
+ * Event payloads use POD fields only. Timestamps are frame numbers in the
+ * owning sequence's frame-rate timebase (same convention as timeline.h);
+ * `handle`/`source` are borrowed pointers the callee may use during the
+ * callback only.
+ */
+
+/**
+ * @brief Event ids for oakengine_event_subscribe().
+ *
+ * handle column: the facade handle to pass for that event.
+ * payload column: oakengine_event field contents on delivery.
+ */
+#define OAKENGINE_EVENT_PROJECT_MODIFIED_CHANGED 1 /**< handle: OakEngineProject*. a = new modified flag (0/1). */
+#define OAKENGINE_EVENT_PROJECT_NAME_CHANGED 2 /**< handle: OakEngineProject*. no payload. */
+
+#define OAKENGINE_EVENT_FOLDER_BEGIN_INSERT_ITEM 10 /**< handle: OakEngineNode* (a folder). handle field = child OakEngineNode*, a = insertion index. */
+#define OAKENGINE_EVENT_FOLDER_END_INSERT_ITEM 11 /**< handle: OakEngineNode* (a folder). no payload. */
+#define OAKENGINE_EVENT_FOLDER_BEGIN_REMOVE_ITEM 12 /**< handle: OakEngineNode* (a folder). handle field = child OakEngineNode*, a = child index. */
+#define OAKENGINE_EVENT_FOLDER_END_REMOVE_ITEM 13 /**< handle: OakEngineNode* (a folder). no payload. */
+
+#define OAKENGINE_EVENT_SEQUENCE_TRACK_ADDED 20 /**< handle: OakEngineSequence*. handle field = OakEngineTrack*, a = track type (OAKENGINE_TRACK_TYPE_*). */
+#define OAKENGINE_EVENT_SEQUENCE_TRACK_REMOVED 21 /**< handle: OakEngineSequence*. handle field = OakEngineTrack*, a = track type. */
+#define OAKENGINE_EVENT_SEQUENCE_TRACK_LIST_CHANGED 22 /**< handle: OakEngineSequence*. a = track type. Fired on TrackList::track_list_changed (order/label-affecting changes). */
+#define OAKENGINE_EVENT_SEQUENCE_TRACK_HEIGHT_CHANGED 23 /**< handle: OakEngineSequence*. handle field = OakEngineTrack*, a = track type, b = new height in PIXELS (TrackList::track_height_changed). */
+#define OAKENGINE_EVENT_SEQUENCE_SUBTITLES_CHANGED 24 /**< handle: OakEngineSequence*. a/b = changed range in/out (ts). */
+
+#define OAKENGINE_EVENT_TRACK_BLOCK_ADDED 30 /**< handle: OakEngineTrack*. handle field = OakEngineBlock*, a = block in-point (ts), b = block out-point (ts). */
+#define OAKENGINE_EVENT_TRACK_BLOCK_REMOVED 31 /**< handle: OakEngineTrack*. handle field = OakEngineBlock*, a = in (ts), b = out (ts) at removal time. */
+#define OAKENGINE_EVENT_TRACK_INDEX_CHANGED 32 /**< handle: OakEngineTrack*. a = old index, b = new index. */
+#define OAKENGINE_EVENT_TRACK_HEIGHT_CHANGED 33 /**< handle: OakEngineTrack*. a = int64 bit-cast of the new height (double, internal units; memcpy to decode). */
+#define OAKENGINE_EVENT_TRACK_BLOCKS_REFRESHED 34 /**< handle: OakEngineTrack*. no payload (Track::blocks_refreshed). */
+#define OAKENGINE_EVENT_TRACK_MUTED_CHANGED 35 /**< handle: OakEngineTrack*. a = muted 0/1. */
+
+#define OAKENGINE_EVENT_BLOCK_ENABLED_CHANGED 36 /**< handle: OakEngineBlock*. no payload (Block::enabled_changed; re-read via oakengine_block_is_enabled). */
+#define OAKENGINE_EVENT_BLOCK_PREVIEW_CHANGED 37 /**< handle: OakEngineBlock*. no payload (Block::preview_changed). */
+
+#define OAKENGINE_EVENT_SEQUENCE_MARKER_ADDED 40 /**< handle: OakEngineSequence*. a = marker in-point (ts). */
+#define OAKENGINE_EVENT_SEQUENCE_MARKER_REMOVED 41 /**< handle: OakEngineSequence*. a = marker in-point (ts). */
+#define OAKENGINE_EVENT_SEQUENCE_MARKER_MODIFIED 42 /**< handle: OakEngineSequence*. a = marker in-point (ts). */
+
+#define OAKENGINE_EVENT_SEQUENCE_WORKAREA_RANGE_CHANGED 50 /**< handle: OakEngineSequence*. a = in (ts), b = out (ts). */
+#define OAKENGINE_EVENT_SEQUENCE_WORKAREA_ENABLED_CHANGED 51 /**< handle: OakEngineSequence*. a = enabled flag (0/1). */
+
+#define OAKENGINE_EVENT_COLOR_MANAGER_CONFIG_CHANGED 60 /**< handle: OakEngineColorManager*. no payload. Fired when the OCIO config changes (ColorManager::config_changed). */
+#define OAKENGINE_EVENT_COLOR_MANAGER_REFERENCE_SPACE_CHANGED 61 /**< handle: OakEngineColorManager*. no payload (ColorManager::reference_space_changed). */
+
+/* Node family (handle: OakEngineNode*). `s` carries the input id where
+ * noted; frame timestamps use the same timebase as oakengine_node_frame_
+ * time_base() (the project's first sequence's frame rate). */
+#define OAKENGINE_EVENT_NODE_LABEL_CHANGED 70 /**< s = new label. */
+#define OAKENGINE_EVENT_NODE_INPUT_VALUE_CHANGED 71 /**< s = input id, a = element, b = range in (ts), c = range out (ts). */
+#define OAKENGINE_EVENT_NODE_INPUT_CONNECTED 72 /**< handle = connected output OakEngineNode*, s = input id, a = element. */
+#define OAKENGINE_EVENT_NODE_INPUT_DISCONNECTED 73 /**< handle = former output OakEngineNode*, s = input id, a = element. */
+#define OAKENGINE_EVENT_NODE_INPUT_FLAGS_CHANGED 74 /**< s = input id, a = new flags (OAKENGINE_NODE_INPUT_FLAG_*). */
+#define OAKENGINE_EVENT_NODE_INPUT_PROPERTY_CHANGED 75 /**< s = input id (property key/value intentionally omitted; re-read through the node family getters). */
+#define OAKENGINE_EVENT_NODE_INPUT_DATA_TYPE_CHANGED 76 /**< s = input id, a = new oak_node_value_type. */
+#define OAKENGINE_EVENT_NODE_INPUT_ARRAY_SIZE_CHANGED 77 /**< s = input id, a = old size, b = new size. */
+#define OAKENGINE_EVENT_NODE_KEYFRAME_ENABLE_CHANGED 78 /**< s = input id, a = element, b = enabled (0/1). */
+#define OAKENGINE_EVENT_NODE_KEYFRAME_ADDED 79 /**< handle = OakEngineKeyframe*, s = input id, a = element, b = track. */
+#define OAKENGINE_EVENT_NODE_KEYFRAME_REMOVED 80 /**< handle = OakEngineKeyframe* (about to die; use the s/a/b fields, do not dereference), s = input id, a = element, b = track. */
+#define OAKENGINE_EVENT_NODE_KEYFRAME_TIME_CHANGED 81 /**< handle = OakEngineKeyframe*. */
+#define OAKENGINE_EVENT_NODE_KEYFRAME_TYPE_CHANGED 82 /**< handle = OakEngineKeyframe*. */
+#define OAKENGINE_EVENT_NODE_KEYFRAME_VALUE_CHANGED 83 /**< handle = OakEngineKeyframe*. */
+#define OAKENGINE_EVENT_NODE_NODE_ADDED_TO_CONTEXT 84 /**< handle = OakEngineNode* added to this context. */
+#define OAKENGINE_EVENT_NODE_NODE_REMOVED_FROM_CONTEXT 85 /**< handle = OakEngineNode* removed from this context. */
+#define OAKENGINE_EVENT_NODE_MESSAGE_COUNT_CHANGED 86 /**< no payload. */
+
+/* Group family (handle: OakEngineNode*, must be a group). For 87/88 the
+ * handle field carries the passthrough's inner node, `s` its input id and
+ * `a` its element. */
+#define OAKENGINE_EVENT_GROUP_INPUT_PASSTHROUGH_ADDED 87 /**< handle = inner OakEngineNode*, s = input id, a = element. */
+#define OAKENGINE_EVENT_GROUP_INPUT_PASSTHROUGH_REMOVED 88 /**< handle = inner OakEngineNode*, s = input id, a = element. */
+#define OAKENGINE_EVENT_GROUP_OUTPUT_PASSTHROUGH_CHANGED 89 /**< handle = new output OakEngineNode*. */
+
+/**
+ * handle = OakEngineNode* whose position in this context changed; `a`/`b`
+ * carry the new x/y scene coordinates as int64 bit-casts of double (use
+ * memcpy to decode). */
+#define OAKENGINE_EVENT_NODE_CONTEXT_POSITION_CHANGED 90
+
+#define OAKENGINE_EVENT_NODE_LINKS_CHANGED 91 /**< no payload (Node::links_changed). */
+#define OAKENGINE_EVENT_NODE_COLOR_CHANGED 92 /**< no payload (Node::color_changed). */
+#define OAKENGINE_EVENT_NODE_INPUT_ADDED 93 /**< s = input id (Node::input_added). */
+#define OAKENGINE_EVENT_NODE_INPUT_REMOVED 94 /**< s = input id (Node::input_removed). */
+#define OAKENGINE_EVENT_NODE_REMOVED_FROM_GRAPH 95 /**< handle = project OakEngineNode* (Node::removed_from_graph). */
+
+/* Viewer family (handle: OakEngineNode*, must be a viewer -- validate with
+ * oakengine_viewer_from_node()). Rational payloads (seconds) are carried
+ * as a = numerator, b = denominator. */
+#define OAKENGINE_EVENT_VIEWER_LENGTH_CHANGED 100 /**< a/b = new length. */
+#define OAKENGINE_EVENT_VIEWER_PLAYHEAD_CHANGED 101 /**< a/b = new playhead. */
+#define OAKENGINE_EVENT_VIEWER_FRAME_RATE_CHANGED 102 /**< a/b = new frame rate (NOT flipped). */
+#define OAKENGINE_EVENT_VIEWER_SIZE_CHANGED 103 /**< a = width, b = height. */
+#define OAKENGINE_EVENT_VIEWER_PIXEL_ASPECT_CHANGED 104 /**< a/b = new pixel aspect. */
+#define OAKENGINE_EVENT_VIEWER_INTERLACING_CHANGED 105 /**< a = olive::VideoParams::Interlacing. */
+#define OAKENGINE_EVENT_VIEWER_VIDEO_PARAMS_CHANGED 106 /**< no payload. */
+#define OAKENGINE_EVENT_VIEWER_AUDIO_PARAMS_CHANGED 107 /**< no payload. */
+#define OAKENGINE_EVENT_VIEWER_TEXTURE_INPUT_CHANGED 108 /**< no payload. */
+#define OAKENGINE_EVENT_VIEWER_SAMPLE_RATE_CHANGED 109 /**< a = new sample rate. */
+#define OAKENGINE_EVENT_VIEWER_CONNECTED_WAVEFORM_CHANGED 110 /**< no payload. */
+
+/* Marker list family (handle: OakEngineMarkerList*, from
+ * oakengine_viewer_get_marker_list()). handle field = the OakEngineMarker*
+ * (for REMOVED it is about to die; do not dereference). */
+#define OAKENGINE_EVENT_MARKER_LIST_MARKER_ADDED 111
+#define OAKENGINE_EVENT_MARKER_LIST_MARKER_REMOVED 112
+#define OAKENGINE_EVENT_MARKER_LIST_MARKER_MODIFIED 113
+
+/* Workarea family (handle: OakEngineWorkarea*, borrowed from
+ * oakengine_viewer_get_workarea_handle() or owned from
+ * oakengine_workarea_create()). */
+#define OAKENGINE_EVENT_WORKAREA_RANGE_CHANGED 114 /**< no payload; re-read via oakengine_workarea_get(). */
+#define OAKENGINE_EVENT_WORKAREA_ENABLED_CHANGED 115 /**< a = enabled 0/1. */
+
+/* Task manager family (handle: oakengine_task_manager_handle(), see
+ * oakengine/task.h). The handle field carries the OakEngineTask* (for
+ * REMOVED it is about to die; do not dereference). */
+#define OAKENGINE_EVENT_TASK_MANAGER_TASK_ADDED 120 /**< handle = OakEngineTask*, s = task title. */
+#define OAKENGINE_EVENT_TASK_MANAGER_TASK_REMOVED 121 /**< handle = OakEngineTask* (about to die; do not dereference). */
+#define OAKENGINE_EVENT_TASK_MANAGER_TASK_FAILED 122 /**< handle = OakEngineTask*. */
+#define OAKENGINE_EVENT_TASK_MANAGER_LIST_CHANGED 123 /**< no payload. */
+
+/* Task family (handle: OakEngineTask*, see oakengine/task.h). Delivered
+ * synchronously on the thread the task runs on. */
+#define OAKENGINE_EVENT_TASK_STARTED 125 /**< a = start time (msecs since epoch). */
+#define OAKENGINE_EVENT_TASK_PROGRESS 126 /**< a = int64 bit-cast of the progress double 0..1 (memcpy to decode). */
+#define OAKENGINE_EVENT_TASK_FINISHED 127 /**< a = succeeded 0/1. */
+
+/* Undo stack family (handle: oakengine_undo_handle(), see
+ * oakengine/undo.h). Fires after every stack mutation (push/undo/redo/
+ * jump/clear); re-read the command list through the oakengine_undo_*
+ * accessors. */
+#define OAKENGINE_EVENT_UNDO_INDEX_CHANGED 130 /**< a = new index (done-command count). */
+
+/* AudioManager family (handle: oakengine_audio_manager_handle(), see
+ * oakengine/audio.h). Fired when the output device or format changes. */
+#define OAKENGINE_EVENT_AUDIO_MANAGER_OUTPUT_PARAMS_CHANGED 140 /**< no payload. */
+#define OAKENGINE_EVENT_AUDIO_MANAGER_OUTPUT_NOTIFY 141 /**< no payload; emitted after each notify interval of audio has been consumed. */
+
+/* ---- Playback cache / frame cache (B9c) ----------------------------------- */
+#define OAKENGINE_EVENT_PLAYBACK_CACHE_INVALIDATED 141
+#define OAKENGINE_EVENT_PLAYBACK_CACHE_VALIDATED 142
+#define OAKENGINE_EVENT_FRAME_CACHE_INVALIDATED 143
+
+/**
+ * @brief POD event payload delivered to oakengine_event_fn.
+ */
+typedef struct oakengine_event {
+ int32_t id; /**< Event id (OAKENGINE_EVENT_*). */
+ int32_t reserved; /**< Alignment padding; 0. */
+ int64_t a; /**< Event-specific integer payload (see the event table). */
+ int64_t b; /**< Event-specific second integer payload. */
+ int64_t c; /**< Event-specific third integer payload. */
+ void *source; /**< The subscribed handle the event was delivered for (borrowed). */
+ void *handle; /**< Related object, event-specific (borrowed; NULL when none). */
+ const char *s; /**< Event-specific string payload (valid only during the callback; NULL when none). */
+} oakengine_event;
+
+/**
+ * @brief Change-notification callback. Invoked synchronously on the
+ * emitting thread; `event` is valid only for the duration of the call.
+ */
+typedef void (*oakengine_event_fn)(const oakengine_event *event,
+ void *userdata);
+
+/**
+ * @brief Subscribe to `event_id` on `handle` (an OakEngineProject*,
+ * OakEngineSequence*, OakEngineTrack* or OakEngineNode* per the event
+ * table) and return a subscription id (> 0).
+ *
+ * Returns 0 on failure: NULL handle/callback, unknown event id, or a
+ * handle whose engine object does not match the event's family. The
+ * callback starts firing with the next matching change; there is no
+ * replay of past state.
+ */
+OAKENGINE_API int64_t oakengine_event_subscribe(void *handle, int32_t event_id,
+ oakengine_event_fn fn,
+ void *userdata);
+
+/**
+ * @brief Cancel a subscription. OAKENGINE_OK on success,
+ * OAKENGINE_E_INVALID for `id` <= 0, OAKENGINE_E_NOT_FOUND for an id that
+ * was never registered or whose engine object has since been destroyed.
+ */
+OAKENGINE_API int oakengine_event_unsubscribe(int64_t id);
+
+#ifdef __cplusplus
+}
+#endif
+
+#endif /* OAKENGINE_EVENTS_H */
diff --git a/engine/include/oakengine/footage.h b/engine/include/oakengine/footage.h
index ed93f7981..ca4deb8ac 100644
--- a/engine/include/oakengine/footage.h
+++ b/engine/include/oakengine/footage.h
@@ -117,6 +117,24 @@ typedef struct oak_footage_audio_info {
int time_base_den; /**< Seconds per time-base unit (denominator). */
} oak_footage_audio_info;
+/**
+ * @brief POD proxy generation parameters (olive::ProxyManager::ProxyParams).
+ *
+ * divider: source resolution divider (1 = use absolute width/height,
+ * 2/4/8 = fraction of the source resolution). extension/preset are the
+ * ffmpeg output container and encoder preset (e.g. "mp4"/"veryfast").
+ */
+typedef struct oak_proxy_params {
+ int width;
+ int height;
+ int divider;
+ int version;
+ int crf;
+ int include_audio; /**< 1/0. */
+ char extension[32];
+ char preset[32];
+} oak_proxy_params;
+
/**
* @brief Probe a media file (decoder, streams, durations, color tags).
*
@@ -417,6 +435,69 @@ oakengine_footage_colorspace_count(const OakEngineFootage *self);
OAKENGINE_API int oakengine_footage_colorspace_at(
const OakEngineFootage *self, int index, char *buf, int buf_size);
+/* ---- Footage extras ------------------------------------------------------- */
+
+/** @brief Filename of the imported footage (buf/size). Returns
+ * OAKENGINE_E_INVALID on NULL. */
+OAKENGINE_API int oakengine_footage_get_filename(const OakEngineFootage *self,
+ char *buf, int buf_size);
+
+/** @brief Get the (track_type, stream_index) for the real stream at
+ * `stream_index_in_footage` (which iterates all streams regardless of type).
+ * Returns OAKENGINE_OK or OAKENGINE_E_NOT_FOUND. */
+OAKENGINE_API int oakengine_footage_get_stream_reference(
+ const OakEngineFootage *self, int stream_index_in_footage,
+ int *out_track_type, int *out_stream_index);
+
+/** @brief Human-readable description of a video stream (buf/size).
+ * Returns OAKENGINE_E_NOT_FOUND for an out-of-range index. */
+OAKENGINE_API int oakengine_footage_describe_video_stream(
+ const OakEngineFootage *self, int video_stream_index, char *buf,
+ int buf_size);
+
+/** @brief Human-readable description of an audio stream (buf/size).
+ * Returns OAKENGINE_E_NOT_FOUND for an out-of-range index. */
+OAKENGINE_API int oakengine_footage_describe_audio_stream(
+ const OakEngineFootage *self, int audio_stream_index, char *buf,
+ int buf_size);
+
+/** @brief Human-readable name of a stream type
+ * (OAKENGINE_TRACK_TYPE_* -> translated name). buf/size convention. */
+OAKENGINE_API int oakengine_footage_stream_type_name(int track_type, char *buf,
+ int buf_size);
+
+/** @brief 1 if the footage has custom proxy parameters, 0 otherwise. */
+OAKENGINE_API int oakengine_footage_has_custom_proxy_params(
+ const OakEngineFootage *self);
+
+/** @brief Fill `out` with the effective proxy parameters
+ * (custom if set, otherwise the application defaults). */
+OAKENGINE_API int oakengine_footage_get_effective_proxy_params(
+ const OakEngineFootage *self, oak_proxy_params *out);
+
+/** @brief Set custom proxy parameters (not undoable). */
+OAKENGINE_API int oakengine_footage_set_custom_proxy_params(
+ OakEngineFootage *self, const oak_proxy_params *params);
+
+/** @brief Clear custom proxy parameters, reverting to defaults. */
+OAKENGINE_API int oakengine_footage_clear_custom_proxy_params(
+ OakEngineFootage *self);
+
+/** @brief Generate a proxy with the given parameters (synchronous).
+ * `path` is the proxy file path, `state` the proxy state (0=missing,
+ * 1=generating, 2=ready, 3=failed), `stream_index` the video stream index,
+ * `enabled` 1/0 to enable proxy, `version` the preset version. */
+OAKENGINE_API int oakengine_footage_set_proxy(OakEngineFootage *self,
+ const char *path, int state,
+ int stream_index, int enabled,
+ int version);
+
+/** @brief Delete the proxy file and reset state. */
+OAKENGINE_API int oakengine_footage_clear_proxy(OakEngineFootage *self);
+
+/** @brief Invalidate the footage (force re-probe on next use). */
+OAKENGINE_API int oakengine_footage_invalidate(OakEngineFootage *self);
+
#ifdef __cplusplus
}
#endif
diff --git a/engine/include/oakengine/gizmo.h b/engine/include/oakengine/gizmo.h
new file mode 100644
index 000000000..de0deea45
--- /dev/null
+++ b/engine/include/oakengine/gizmo.h
@@ -0,0 +1,163 @@
+/***
+
+ 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 .
+
+***/
+
+#ifndef OAKENGINE_GIZMO_H
+#define OAKENGINE_GIZMO_H
+
+#include
+
+#include "export.h"
+#include "node.h"
+
+#ifdef __cplusplus
+extern "C" {
+#endif
+
+/**
+ * @file gizmo.h
+ * @brief C ABI for gizmo data exchange (text gizmo POD + draggable helpers)
+ *
+ * TextGizmo has been POD-ified: the app retrieves a flat snapshot of the
+ * text v3 node's gizmo state through a single C call instead of holding a
+ * C++ TextGizmo pointer. The 4 Qt signals (activated/deactivated/
+ * rect_changed/vertical_alignment_changed) are replaced by the existing
+ * OAKENGINE_EVENT_NODE_INPUT_VALUE_CHANGED on the text v3 node.
+ *
+ * DraggableGizmo's drag lifecycle (start/move/end) is exposed as thin C
+ * wrappers so the app can drive dragging without importing engine C++ symbols.
+ */
+
+/**
+ * @brief Flat snapshot of a text v3 node's gizmo state.
+ *
+ * Retrieved via oakengine_text_gizmo_get(). The HTML content is accessed
+ * separately through oakengine_text_gizmo_get_html() because it is a
+ * variable-length string.
+ */
+typedef struct oakengine_text_gizmo {
+ double rect_x; /**< Bounding rect left */
+ double rect_y; /**< Bounding rect top */
+ double rect_w; /**< Bounding rect width */
+ double rect_h; /**< Bounding rect height */
+ int vertical_alignment; /**< 0 = AlignTop, 1 = AlignBottom, 2 = AlignVCenter */
+} oakengine_text_gizmo;
+
+/**
+ * @brief Retrieve the text gizmo POD from a TextGeneratorV3 node.
+ *
+ * @param node The text v3 node (OakEngineNode*). Must be a TextGeneratorV3
+ * (checked at runtime; returns OAKENGINE_E_INVALID otherwise).
+ * @param time_num Numerator of the rational time at which to evaluate.
+ * @param time_den Denominator of the rational time.
+ * @param out Output struct filled on success.
+ * @return OAKENGINE_OK on success, OAKENGINE_E_INVALID if node is not a
+ * TextGeneratorV3 or out is NULL.
+ */
+OAKENGINE_API int oakengine_text_gizmo_get(OakEngineNode *node,
+ int64_t time_num, int64_t time_den, oakengine_text_gizmo *out);
+
+/**
+ * @brief Retrieve the text gizmo's HTML content as a string.
+ *
+ * buf/size convention: pass NULL/0 to get the required length (including
+ * NUL terminator). On success returns the number of bytes written (excluding
+ * NUL). Requires a valid TextGeneratorV3 node.
+ */
+OAKENGINE_API int oakengine_text_gizmo_get_html(OakEngineNode *node,
+ int64_t time_num, int64_t time_den, char *buf, int buf_size);
+
+/**
+ * @brief Update the HTML content of a text v3 node's text input (undoable).
+ *
+ * Equivalent to the old TextGizmo::update_input_html().
+ */
+OAKENGINE_API int oakengine_text_gizmo_update_html(OakEngineNode *node,
+ const char *html, int64_t time_num, int64_t time_den);
+
+/**
+ * @brief Set the vertical alignment of a text v3 node (undoable).
+ *
+ * `alignment`: 0 = AlignTop, 1 = AlignBottom, 2 = AlignVCenter.
+ * Equivalent to the old TextGizmo::set_vertical_alignment().
+ */
+OAKENGINE_API int oakengine_text_gizmo_set_vertical_alignment(
+ OakEngineNode *node, int alignment);
+
+/**
+ * @brief Notify that a text gizmo has been activated (emits the equivalent of
+ * the old TextGizmo::activated signal via event mechanism).
+ *
+ * Currently a no-op since activation events are app-internal; kept for
+ * API completeness.
+ */
+OAKENGINE_API int oakengine_text_gizmo_activated(OakEngineNode *node);
+
+/**
+ * @brief Notify that a text gizmo has been deactivated.
+ */
+OAKENGINE_API int oakengine_text_gizmo_deactivated(OakEngineNode *node);
+
+/**
+ * @brief Activate/Deactivate the text gizmo on a text v3 node.
+ *
+ * These replace the old TextGizmo::activated()/deactivated() signal emissions.
+ * The app calls these to notify the engine that the text editor opened/closed.
+ */
+
+/**
+ * @brief Get the DragValueBehavior of a gizmo node.
+ *
+ * Returns: 0 = k_absolute, 1 = k_delta_from_previous, 2 = k_delta_from_start.
+ * Returns OAKENGINE_E_INVALID if the gizmo is not a DraggableGizmo.
+ */
+OAKENGINE_API int oakengine_gizmo_get_drag_value_behavior(void *gizmo);
+
+/**
+ * @brief Start a drag on a DraggableGizmo.
+ *
+ * Wraps DraggableGizmo::drag_start(). The gizmo's internal NodeInputDraggers
+ * are started at the given time. `row` is a pointer to a NodeValueRow
+ * (populated e.g. by oakengine_traverse_generate_row); pass NULL for an
+ * empty row.
+ */
+OAKENGINE_API int oakengine_gizmo_drag_start(void *gizmo,
+ void *row, double abs_x, double abs_y, int64_t time_num,
+ int64_t time_den);
+
+/**
+ * @brief Move a drag (emits handle_movement signal on the gizmo).
+ */
+OAKENGINE_API int oakengine_gizmo_drag_move(void *gizmo,
+ double x, double y, int qt_keyboard_modifiers);
+
+/**
+ * @brief End a drag and push an undoable command.
+ *
+ * @param gizmo The DraggableGizmo pointer (void* for C ABI).
+ * @param command A MultiUndoCommand* (void*) to append undo entries to.
+ * Pass NULL to create a standalone command.
+ */
+OAKENGINE_API int oakengine_gizmo_drag_end(void *gizmo, void *command);
+
+#ifdef __cplusplus
+}
+#endif
+
+#endif // OAKENGINE_GIZMO_H
diff --git a/engine/include/oakengine/lut.h b/engine/include/oakengine/lut.h
new file mode 100644
index 000000000..354d26260
--- /dev/null
+++ b/engine/include/oakengine/lut.h
@@ -0,0 +1,87 @@
+/***
+
+ 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 .
+
+***/
+
+#ifndef OAKENGINE_LUT_H
+#define OAKENGINE_LUT_H
+
+#include "export.h"
+#include "init.h"
+
+#ifdef __cplusplus
+extern "C" {
+#endif
+
+/**
+ * @file lut.h
+ * @brief C ABI for the global LUT file library (olive::LUTLibrary)
+ *
+ * A thin facade over the user-configurable list of LUT directories and the
+ * supported LUT files discovered under them. The library state is kept in the
+ * application config ("LUTLibraryPaths"); this facade only exposes the
+ * directory/file list queries and the directory replacement primitive.
+ *
+ * Conventions match the other facade families:
+ * - 0 (OAKENGINE_OK) / negative OAKENGINE_E_* codes.
+ * - String output uses the buf/size convention.
+ * - Count queries return a non-negative integer, or a negative error code.
+ */
+
+/**
+ * @brief Number of directories currently in the LUT library.
+ */
+OAKENGINE_API int oakengine_lut_directory_count(void);
+
+/**
+ * @brief Get the directory path at `index` (buf/size convention).
+ *
+ * Returns the string length on success, or a negative OAKENGINE_E_* code when
+ * `index` is out of range.
+ */
+OAKENGINE_API int oakengine_lut_directory_at(int index, char *buf,
+ int buf_size);
+
+/**
+ * @brief Number of supported LUT files found under the library directories.
+ */
+OAKENGINE_API int oakengine_lut_file_count(void);
+
+/**
+ * @brief Get the full path of the LUT file at `index` (buf/size convention).
+ *
+ * Files are listed in the order they are discovered; files in earlier
+ * directories come first. Returns the string length on success, or a negative
+ * OAKENGINE_E_* code when `index` is out of range.
+ */
+OAKENGINE_API int oakengine_lut_file_at(int index, char *buf, int buf_size);
+
+/**
+ * @brief Replace the LUT library directories and persist them to config.
+ *
+ * `dirs` is an array of `count` NUL-terminated UTF-8 directory paths. Passing
+ * `count == 0` clears the library. Returns OAKENGINE_OK or an error code.
+ */
+OAKENGINE_API int oakengine_lut_set_directories(const char *const *dirs,
+ int count);
+
+#ifdef __cplusplus
+}
+#endif
+
+#endif /* OAKENGINE_LUT_H */
diff --git a/engine/include/oakengine/node.h b/engine/include/oakengine/node.h
index 598509891..b902d55e1 100644
--- a/engine/include/oakengine/node.h
+++ b/engine/include/oakengine/node.h
@@ -26,6 +26,7 @@
#include "export.h"
#include "init.h"
#include "project.h"
+#include "videoparams.h"
#ifdef __cplusplus
extern "C" {
@@ -74,7 +75,16 @@ typedef enum oak_node_value_type {
OAK_NODE_VALUE_VEC3, /**< f[0..2] (olive k_vec3) */
OAK_NODE_VALUE_VEC4, /**< f[0..3] (olive k_vec4) */
OAK_NODE_VALUE_COMBO, /**< num = selected index (olive k_combo) */
- OAK_NODE_VALUE_STRING /**< k_file; string APIs only, never in the POD */
+ OAK_NODE_VALUE_STRING, /**< k_file; string APIs only, never in the POD */
+ OAK_NODE_VALUE_TEXT, /**< k_text; string APIs only */
+ OAK_NODE_VALUE_FONT, /**< k_font; string APIs only */
+ OAK_NODE_VALUE_STR_COMBO, /**< k_str_combo; string APIs only */
+ OAK_NODE_VALUE_BINARY, /**< k_binary; binary data, no POD representation */
+ OAK_NODE_VALUE_BEZIER, /**< k_bezier; bezier control point */
+ OAK_NODE_VALUE_TEXTURE, /**< k_texture; texture */
+ OAK_NODE_VALUE_SAMPLES, /**< k_samples; audio samples */
+ OAK_NODE_VALUE_VIDEO_PARAMS, /**< k_video_params; video parameters */
+ OAK_NODE_VALUE_AUDIO_PARAMS /**< k_audio_params; audio parameters */
} oak_node_value_type;
/**
@@ -93,6 +103,16 @@ typedef struct oak_node_value {
*/
typedef struct OakEngineNode OakEngineNode;
+/**
+ * @brief Opaque keyframe handle (borrowed from the input's track list).
+ */
+typedef struct OakEngineKeyframe OakEngineKeyframe;
+
+/**
+ * @brief Opaque input-dragger handle (created by oakengine_dragger_create()).
+ */
+typedef struct OakEngineNodeDragger OakEngineNodeDragger;
+
/**
* @brief Human-readable reason for the last failed node call on this
* thread (buf/size convention).
@@ -112,6 +132,44 @@ OAKENGINE_API int oakengine_project_node_count(const OakEngineProject *self);
OAKENGINE_API OakEngineNode *
oakengine_project_node_at(const OakEngineProject *self, int index);
+/* ---- Node factory --------------------------------------------------------- */
+
+/**
+ * @brief Number of registered node types (wraps NodeFactory::get_library().size()).
+ */
+OAKENGINE_API int oakengine_node_factory_id_count(void);
+
+/**
+ * @brief Create a node of `type_id` WITHOUT adding it to any project.
+ * The caller owns the returned handle and must add it to a project (e.g.
+ * via oakengine_project_add_node or a custom undo command) before the
+ * engine can manage its lifecycle.
+ *
+ * Returns NULL when `type_id` is unknown.
+ */
+OAKENGINE_API OakEngineNode *
+oakengine_node_factory_create_from_id(const char *type_id);
+
+/**
+ * @brief The display name (translated) of the node type identified by
+ * `type_id`, or an empty string when the id is unknown (buf/size
+ * convention).
+ */
+OAKENGINE_API int oakengine_node_factory_name_from_id(const char *type_id,
+ char *buf,
+ int buf_size);
+
+/**
+ * @brief Borrowed pointer to the prototype node at `index` in the
+ * registered library, or NULL when out of range.
+ *
+ * The returned handle is a prototype instance owned by the engine's
+ * NodeFactory; do not delete it or add it to a project. Use it only
+ * for read-only metadata queries (name, category, flags, etc.).
+ */
+OAKENGINE_API OakEngineNode *
+oakengine_node_factory_node_at(int index);
+
/* ---- Metadata -------------------------------------------------------------- */
/**
@@ -164,6 +222,29 @@ OAKENGINE_API int oakengine_node_set_label_many(OakEngineNode **nodes,
int count,
const char *label);
+/**
+ * @brief Set one label on several nodes at once, with optional parent
+ * MultiUndoCommand for composition (like Core::label_nodes() with a
+ * non-NULL parent).
+ *
+ * When `parent_multi_or_NULL` is non-NULL, the new NodeRenameCommand is
+ * added as a child of that MultiUndoCommand and is NOT pushed onto the
+ * global undo stack. The caller is responsible for pushing the parent.
+ * When `parent_multi_or_NULL` is NULL, behavior matches
+ * oakengine_node_set_label_many().
+ */
+OAKENGINE_API int oakengine_node_rename_many(OakEngineNode **nodes,
+ int count,
+ const char *label,
+ void *parent_multi_or_NULL);
+
+/**
+ * @brief Create a NodeRenameCommand as an opaque command pointer for a single
+ * node. Returns NULL on invalid arguments.
+ */
+OAKENGINE_API void *oakengine_node_rename_command(OakEngineNode *node,
+ const char *label);
+
/**
* @brief Set the color-label index of several nodes at once (undoable,
* ONE command; olive::NodeOverrideColorCommand per node, like the
@@ -172,6 +253,13 @@ OAKENGINE_API int oakengine_node_set_label_many(OakEngineNode **nodes,
OAKENGINE_API int oakengine_node_set_color_label(OakEngineNode **nodes,
int count, int color_index);
+/**
+ * @brief Create a NodeOverrideColorCommand as an opaque command pointer
+ * without executing or pushing it.
+ */
+OAKENGINE_API void *oakengine_node_set_color_label_command(
+ OakEngineNode *node, int color_index);
+
/**
* @brief The node's color-label index (Node::get_override_color(); -1 =
* none). -1 on a NULL handle.
@@ -251,6 +339,33 @@ OAKENGINE_API int oakengine_node_set_input_string(OakEngineNode *self,
const char *input_id,
const char *s);
+/**
+ * @brief Create a NodeParamSetStandardValueCommand as an opaque command pointer.
+ * Sets the standard value of `input_id` on `track` (track -1 writes the whole
+ * single-track value). Returns NULL on invalid arguments or type mismatch.
+ */
+OAKENGINE_API void *oakengine_node_set_standard_value_command(
+ OakEngineNode *self, const char *input_id, int element, int track,
+ const oak_node_value *v);
+
+/**
+ * @brief Create a command that sets an input's value at a rational time
+ * (olive::Node::set_value_at_time) as an opaque command pointer. `time_num`
+ * / `time_den` are rational seconds. The returned command is a
+ * MultiUndoCommand; add it to a parent or push it with oakengine_undo_push().
+ */
+OAKENGINE_API void *oakengine_node_set_value_at_time_command(
+ void *node, const char *input, int element, int64_t time_num,
+ int64_t time_den, const oak_node_value *value, int track,
+ int insert_on_all_tracks_if_no_key);
+
+/**
+ * @brief Create a NodeParamSetStandardValueCommand for a k_video_params input
+ * as an opaque command pointer. `params` must describe a valid VideoParams.
+ */
+OAKENGINE_API void *oakengine_node_set_input_video_params_command(
+ OakEngineNode *self, const char *input_id, const oak_video_params *params);
+
/**
* @brief The frame timebase used for keyframe/parameter frame timestamps
* (seconds per frame: the frame rate of the project's first sequence
@@ -359,6 +474,52 @@ OAKENGINE_API int oakengine_node_disconnect_ex(OakEngineNode *input_node,
const char *input_id,
int element);
+/**
+ * @brief Create a NodeEdgeAddCommand as an opaque command pointer without
+ * executing or pushing it. Ownership passes to the caller; add it to a
+ * MultiUndoCommand with oakengine_undo_command_multi_add_child() or push
+ * it with oakengine_undo_push(). `element` is -1 for non-array inputs.
+ */
+OAKENGINE_API void *oakengine_node_connect_command(OakEngineNode *output_node,
+ OakEngineNode *input_node,
+ const char *input_id,
+ int element);
+
+/**
+ * @brief Create a NodeEdgeRemoveCommand as an opaque command pointer without
+ * executing or pushing it.
+ */
+OAKENGINE_API void *oakengine_node_disconnect_command(
+ OakEngineNode *input_node, const char *input_id, int element);
+
+/**
+ * @brief Link or unlink two blocks/nodes directly (olive::Node::link/unlink).
+ * Returns 1 on success, 0 on failure, OAKENGINE_E_INVALID if either pointer
+ * is NULL.
+ */
+OAKENGINE_API int oakengine_block_link(void *a, void *b, int linked);
+
+/**
+ * @brief Create a NodeAddCommand as an opaque command pointer without executing
+ * or pushing it. Adds an existing `node` to `project` on redo.
+ */
+OAKENGINE_API void *oakengine_node_add_to_project_command(
+ OakEngineProject *project, OakEngineNode *node);
+
+/**
+ * @brief Set a traverse value hint on an input (Node::set_value_hint_for_input()).
+ *
+ * `type` is an oak_node_value_type (0-19), or -1 to match the input's declared
+ * type. `index` is the traverse table row index (-1 for auto-detect). `tag` is
+ * an optional string hint (may be NULL). Returns OAKENGINE_OK on success,
+ * OAKENGINE_E_INVALID for NULL/type-999-style args, OAKENGINE_E_NOT_FOUND for
+ * an unknown input id.
+ */
+OAKENGINE_API int oakengine_node_set_value_hint(OakEngineNode *self,
+ const char *input_id,
+ int element, int type,
+ int index, const char *tag);
+
/* ---- Parameter animation (keyframes) --------------------------------------
*
* Keyframes live on an input's keyframe tracks (olive::NodeKeyframe). All
@@ -437,6 +598,37 @@ OAKENGINE_API int oakengine_node_keyframe_remove(OakEngineNode *self,
const char *input_id,
int64_t time_ts);
+/**
+ * @brief Create a NodeParamInsertKeyframeCommand as an opaque command pointer.
+ */
+OAKENGINE_API void *oakengine_node_insert_keyframe_command(
+ OakEngineNode *self, const char *input_id, int element, int track,
+ int64_t time_ts, const oak_node_value *value, int type, float x1, float y1,
+ float x2, float y2);
+
+/**
+ * @brief Create a NodeParamRemoveKeyframeCommand as an opaque command pointer
+ * from a borrowed keyframe handle.
+ */
+OAKENGINE_API void *oakengine_node_remove_keyframe_command(
+ OakEngineKeyframe *keyframe);
+
+/**
+ * @brief Create a NodeParamSetKeyframeTimeCommand as an opaque command pointer
+ * from a borrowed keyframe handle. The previous time is captured at apply
+ * time; `new_time_ts` is in the project's frame timestamp timebase.
+ */
+OAKENGINE_API void *oakengine_keyframe_set_time_command(
+ OakEngineKeyframe *keyframe, int64_t new_time_ts);
+
+/**
+ * @brief Create a NodeParamSetKeyframeValueCommand as an opaque command pointer
+ * from a borrowed keyframe handle. The previous value is captured at apply
+ * time. `value->type` must match the keyframe's declared input type.
+ */
+OAKENGINE_API void *oakengine_keyframe_set_value_command(
+ OakEngineKeyframe *keyframe, const oak_node_value *value);
+
/**
* @brief Change the easing of the keyframe at `time_ts` (undoable; set type
* plus bezier control points, mirroring the application's keyframe view
@@ -538,8 +730,894 @@ OAKENGINE_API int oakengine_node_keyframe_set_bezier_point(
OAKENGINE_API int oakengine_node_keyframes_clear(OakEngineNode *self,
const char *input_id);
+/* ---- Extended input introspection ----------------------------------------- */
+
+/**
+ * @brief 1 if the input is an array-type input.
+ */
+OAKENGINE_API int oakengine_node_input_is_array(
+ const OakEngineNode *self, const char *input_id);
+
+/**
+ * @brief Number of elements in the array input (0 for non-array inputs).
+ */
+OAKENGINE_API int oakengine_node_input_array_size(
+ const OakEngineNode *self, const char *input_id);
+
+/**
+ * @brief The input's flags bitmask (Node::get_input_flags(); 0 on NULL).
+ */
+OAKENGINE_API int oakengine_node_input_get_flags(
+ const OakEngineNode *self, const char *input_id);
+
+/**
+ * @brief 1 if the input can accept a connection (connectable).
+ */
+OAKENGINE_API int oakengine_node_input_is_connectable(
+ const OakEngineNode *self, const char *input_id);
+
+/**
+ * @brief 1 if the input supports keyframing (keyframable).
+ */
+OAKENGINE_API int oakengine_node_input_is_keyframable(
+ const OakEngineNode *self, const char *input_id);
+
+/**
+ * @brief 1 if keyframing is enabled for this input on any track
+ * (keyframed_ex; pass -1 for all tracks).
+ */
+OAKENGINE_API int oakengine_node_input_is_keyframed_ex(
+ const OakEngineNode *self, const char *input_id, int track);
+
+/**
+ * @brief The node's label and name combined (buf/size).
+ */
+OAKENGINE_API int oakengine_node_get_label_and_name(
+ const OakEngineNode *self, char *buf, int buf_size);
+
+/**
+ * @brief The human-readable name of the input (buf/size).
+ */
+OAKENGINE_API int oakengine_node_get_input_name(
+ const OakEngineNode *self, const char *input_id, char *buf,
+ int buf_size);
+
+/**
+ * @brief The default value of the input at a track index.
+ */
+OAKENGINE_API int oakengine_node_input_get_default_value(
+ const OakEngineNode *self, const char *input_id, int track,
+ oak_node_value *out);
+
+/**
+ * @brief The project that owns this node (NULL on NULL input).
+ */
+OAKENGINE_API OakEngineProject *oakengine_node_get_project(
+ const OakEngineNode *self);
+
+/**
+ * @brief The node connected to the input, or NULL (element -1 for
+ * non-array inputs).
+ */
+OAKENGINE_API OakEngineNode *oakengine_node_input_get_connected_node(
+ const OakEngineNode *self, const char *input_id, int element);
+
+/**
+ * @brief Copy the values (not connections) from `src` to `dest`
+ * (undoable, ONE command).
+ */
+OAKENGINE_API int oakengine_node_copy_inputs(
+ OakEngineNode *dest, const OakEngineNode *src);
+
+/**
+ * @brief Get the value of an input at a specific time (frame timestamp
+ * timebase). String inputs fail with OAKENGINE_E_INVALID.
+ */
+OAKENGINE_API int oakengine_node_get_input_at_time(
+ const OakEngineNode *self, const char *input_id, int element, int track,
+ int64_t time_ts, int track_for_time, oak_node_value *out);
+
+/**
+ * @brief Get a string input's value at a specific time (buf/size).
+ */
+OAKENGINE_API int oakengine_node_get_input_string_at_time(
+ const OakEngineNode *self, const char *input_id, int element,
+ int64_t time_ts, int track, char *buf, int buf_size);
+
+/**
+ * @brief Get the bezier value of an input at a specific time (fails with
+ * E_INVALID for non-bezier inputs).
+ */
+OAKENGINE_API int oakengine_node_get_input_bezier_at_time(
+ const OakEngineNode *self, const char *input_id, int element,
+ int64_t time_ts, int track, double *out_6);
+
+/**
+ * @brief Get the binary value of an input at a specific time (fails with
+ * E_INVALID for non-binary inputs).
+ */
+OAKENGINE_API int oakengine_node_get_input_binary_at_time(
+ const OakEngineNode *self, const char *input_id, int element,
+ int64_t time_ts, int track, char *buf, int buf_size);
+
+/* ---- Input properties ----------------------------------------------------- */
+
+/**
+ * @brief 1 if the input has a property with the given key.
+ */
+OAKENGINE_API int oakengine_node_input_has_property(
+ const OakEngineNode *self, const char *input_id, const char *key);
+
+/**
+ * @brief Set a string property on an input (undoable; notify != 0 sends
+ * change notification).
+ */
+OAKENGINE_API int oakengine_node_set_input_property_string(
+ OakEngineNode *self, const char *input_id, const char *key,
+ const char *value, int notify);
+
+/**
+ * @brief Read a string property (buf/size).
+ */
+OAKENGINE_API int oakengine_node_input_get_property_string(
+ const OakEngineNode *self, const char *input_id, const char *key,
+ char *buf, int buf_size);
+
+/**
+ * @brief Read a numeric property as a double (-1 track = whole value).
+ */
+OAKENGINE_API int oakengine_node_input_get_property_number(
+ const OakEngineNode *self, const char *input_id, const char *key,
+ int track, double *out);
+
+/**
+ * @brief Read an integer property.
+ */
+OAKENGINE_API int oakengine_node_input_get_property_int(
+ const OakEngineNode *self, const char *input_id, const char *key,
+ int64_t *out);
+
+/**
+ * @brief Read a rational property (numerator/denominator; any may be NULL).
+ */
+OAKENGINE_API int oakengine_node_input_get_property_rational(
+ const OakEngineNode *self, const char *input_id, const char *key,
+ int *num, int *den);
+
+/**
+ * @brief The number of properties on the input.
+ */
+OAKENGINE_API int oakengine_node_input_get_property_count(
+ const OakEngineNode *self, const char *input_id);
+
+/**
+ * @brief Enumerate the property key at `index` (buf/size; 0-based index
+ * into the property map). Returns the length on success, negative on error.
+ */
+OAKENGINE_API int oakengine_node_input_get_property_key(
+ const OakEngineNode *self, const char *input_id, int index,
+ char *buf, int buf_size);
+
+/**
+ * @brief The number of elements in a string-list property.
+ */
+OAKENGINE_API int oakengine_node_input_get_property_string_list_count(
+ const OakEngineNode *self, const char *input_id, const char *key);
+
+/**
+ * @brief Read one element of a string-list property (buf/size).
+ */
+OAKENGINE_API int oakengine_node_input_get_property_string_list(
+ const OakEngineNode *self, const char *input_id, const char *key,
+ int index, char *buf, int buf_size);
+
+/* ---- Node type queries ---------------------------------------------------- */
+
+/**
+ * @brief 1 if the node is a group node.
+ */
+OAKENGINE_API int oakengine_node_is_group(const OakEngineNode *self);
+
+/**
+ * @brief 1 if the node is a multi-camera node.
+ */
+OAKENGINE_API int oakengine_node_is_multicam(const OakEngineNode *self);
+
+/* ---- Context positions ---------------------------------------------------- */
+
+/**
+ * @brief The number of nodes visible in the given context
+ * (Node::context_count() for the underlying context; -1 on NULL context).
+ */
+OAKENGINE_API int oakengine_node_context_node_count(
+ const OakEngineNode *context);
+
+/**
+ * @brief 1 if the context contains the node.
+ */
+OAKENGINE_API int oakengine_node_context_contains_node(
+ const OakEngineNode *context, const OakEngineNode *node);
+
+/**
+ * @brief The node at an index in the context (NULL when out of range;
+ * returns x/y/expanded pointers if non-NULL).
+ */
+OAKENGINE_API OakEngineNode *oakengine_node_context_node_at(
+ OakEngineNode *context, int index, double *x, double *y,
+ int *expanded);
+
+/**
+ * @brief Set the context position of a node (undoable).
+ */
+OAKENGINE_API int oakengine_node_set_context_position(
+ OakEngineNode *context, OakEngineNode *node, double x, double y);
+
+/**
+ * @brief Get the context position of a node.
+ */
+OAKENGINE_API int oakengine_node_get_context_position(
+ const OakEngineNode *context, const OakEngineNode *node,
+ double *x, double *y, int *expanded);
+
+/**
+ * @brief Set the expanded flag of a node in a context (undoable).
+ */
+OAKENGINE_API int oakengine_node_set_context_expanded(
+ OakEngineNode *context, OakEngineNode *node, int expanded);
+
+/* ---- Effect input --------------------------------------------------------- */
+
+/**
+ * @brief Get the node's effect input id and element (typically the texture
+ * input for generators/filters). OAKENGINE_E_NOT_FOUND when none.
+ */
+OAKENGINE_API int oakengine_node_get_effect_input(
+ const OakEngineNode *self, char *input_id, int input_id_size,
+ int *element);
+
+/* ---- Group passthrough ---------------------------------------------------- */
+
+/**
+ * @brief Create a detached group node (equivalent to new NodeGroup()).
+ * The caller owns the returned handle and must add it to a project before
+ * the engine manages its lifecycle.
+ */
+OAKENGINE_API OakEngineNode *oakengine_node_group_create(void);
+
+/**
+ * @brief Walk one group-passthrough level: if `*inout_node` is a group and
+ * its input `*inout_input`/`*inout_element` is a passthrough, replace them
+ * with the inner node/input/element and return 1. Returns 0 when the node
+ * is not a group or the input is not a passthrough (inouts unchanged).
+ */
+OAKENGINE_API int oakengine_node_group_get_inner(
+ OakEngineNode **inout_node, char *inout_input, int inout_input_size,
+ int *inout_element);
+
+/**
+ * @brief The number of input passthroughs on the group (OAKENGINE_E_INVALID
+ * when the node is not a group).
+ */
+OAKENGINE_API int oakengine_group_input_passthrough_count(
+ const OakEngineNode *self);
+
+/**
+ * @brief Add an input passthrough to the group (direct, no undo).
+ */
+OAKENGINE_API int oakengine_group_add_input_passthrough(
+ OakEngineNode *self, OakEngineNode *inner_node,
+ const char *inner_input, int inner_element,
+ const char *preferred_id, char *out_id, int out_id_size);
+
+/**
+ * @brief Read the i-th input passthrough of the group.
+ */
+OAKENGINE_API int oakengine_group_input_passthrough_at(
+ const OakEngineNode *self, int index, char *id, int id_size,
+ OakEngineNode **node, char *input_id, int input_id_size,
+ int *element);
+
+/**
+ * @brief Look up a passthrough id by (node, input, element).
+ */
+OAKENGINE_API int oakengine_group_get_id_of_passthrough(
+ const OakEngineNode *self, OakEngineNode *inner_node,
+ const char *inner_input, int inner_element, char *id, int id_size);
+
+/**
+ * @brief Look up (node, input, element) by passthrough id.
+ */
+OAKENGINE_API int oakengine_group_get_passthrough_from_id(
+ const OakEngineNode *self, const char *id, OakEngineNode **out_node,
+ char *out_input, int out_input_size, int *out_element);
+
+/**
+ * @brief Get the output passthrough node (or NULL).
+ */
+OAKENGINE_API OakEngineNode *oakengine_group_get_output_passthrough(
+ const OakEngineNode *self);
+
+/**
+ * @brief Set the output passthrough node (direct, no undo).
+ */
+OAKENGINE_API int oakengine_group_set_output_passthrough(
+ OakEngineNode *self, OakEngineNode *inner_node);
+
+/**
+ * @brief Resolve a passthrough id to its real node and input (handles
+ * nested groups).
+ */
+OAKENGINE_API int oakengine_group_resolve_input(
+ const OakEngineNode *self, const char *id, int element,
+ OakEngineNode **out_node, char *out_input, int out_input_size,
+ int *out_element);
+
+/**
+ * @brief Remove an input passthrough (direct, no undo).
+ */
+OAKENGINE_API int oakengine_group_remove_input_passthrough(
+ OakEngineNode *self, OakEngineNode *inner_node,
+ const char *inner_input, int inner_element);
+
+/**
+ * @brief Create a NodeGroupAddInputPassthrough command as an opaque command
+ * pointer without executing or pushing it.
+ */
+OAKENGINE_API void *oakengine_group_add_input_passthrough_command(
+ OakEngineNode *self, OakEngineNode *inner_node,
+ const char *inner_input, int inner_element,
+ const char *preferred_id);
+
+/**
+ * @brief Create a NodeGroupSetOutputPassthrough command as an opaque command
+ * pointer without executing or pushing it.
+ */
+OAKENGINE_API void *oakengine_group_set_output_passthrough_command(
+ OakEngineNode *self, OakEngineNode *inner_node);
+
+/**
+ * @brief Add an input passthrough (undoable; ONE undoable command).
+ */
+OAKENGINE_API int oakengine_group_add_input_passthrough_undoable(
+ OakEngineNode *self, OakEngineNode *inner_node,
+ const char *inner_input, int inner_element,
+ const char *preferred_id);
+
+/**
+ * @brief Set the output passthrough node (undoable; ONE undoable command).
+ */
+OAKENGINE_API int oakengine_group_set_output_passthrough_undoable(
+ OakEngineNode *self, OakEngineNode *inner_node);
+
+/* ---- Multi-camera --------------------------------------------------------- */
+
+/**
+ * @brief The input id string for the current camera.
+ */
+OAKENGINE_API const char *oakengine_multicam_input_current(void);
+
+/**
+ * @brief The input id string for the sources array.
+ */
+OAKENGINE_API const char *oakengine_multicam_input_sources(void);
+
+/**
+ * @brief The input id string for the sequence.
+ */
+OAKENGINE_API const char *oakengine_multicam_input_sequence(void);
+
+/**
+ * @brief The input id string for the sequence type.
+ */
+OAKENGINE_API const char *oakengine_multicam_input_sequence_type(void);
+
+/**
+ * @brief Number of connected source cameras (OAKENGINE_E_INVALID when
+ * the node is not a multicam).
+ */
+OAKENGINE_API int oakengine_multicam_get_source_count(
+ const OakEngineNode *self);
+
+/**
+ * @brief Compute the grid (rows, cols) for the given number of sources.
+ */
+OAKENGINE_API int oakengine_multicam_get_rows_and_columns(
+ int source_count, int *rows, int *cols);
+
+/**
+ * @brief Convert a flat index to (row, col) in the grid.
+ */
+OAKENGINE_API int oakengine_multicam_index_to_row_cols(
+ int index, int rows, int cols, int *out_row, int *out_col);
+
+/**
+ * @brief Convert (row, col) to a flat index.
+ */
+OAKENGINE_API int oakengine_multicam_rows_cols_to_index(
+ int row, int col, int rows, int cols);
+
+/**
+ * @brief Current source index of a multicam node.
+ * Returns the index or OAKENGINE_E_INVALID when `node` is not a multicam.
+ */
+OAKENGINE_API int oakengine_multicam_get_current_source(
+ const OakEngineNode *node);
+
+/* ---- Shape node ----------------------------------------------------------- */
+
+/**
+ * @brief Set a shape node's rectangle (undoable). `x`/`y`/`w`/`h` are in
+ * pixels; `video_params` is an oak_video_params POD describing the target
+ * resolution. Returns OAKENGINE_OK or OAKENGINE_E_INVALID.
+ */
+OAKENGINE_API int oakengine_shape_set_rect_undoable(
+ OakEngineNode *node, double x, double y, double w, double h,
+ const oak_video_params *video_params, void *command);
+
+/* ---- Subtitle block ------------------------------------------------------- */
+
+/** @brief The input id string for the subtitle text input. */
+OAKENGINE_API const char *oakengine_subtitle_text_input_id(void);
+
+/**
+ * @brief Get the subtitle block's text (buf/size convention).
+ * Returns the would-be length (excluding NUL) or OAKENGINE_E_INVALID.
+ */
+OAKENGINE_API int oakengine_subtitle_get_text(const OakEngineNode *node,
+ char *buf, int buf_size);
+
+/**
+ * @brief Set the subtitle block's text (non-undoable).
+ * Returns OAKENGINE_OK or OAKENGINE_E_INVALID.
+ */
+OAKENGINE_API int oakengine_subtitle_set_text(OakEngineNode *node,
+ const char *text);
+
+/* ---- Bulk graph deletion -------------------------------------------------- */
+
+/**
+ * @brief Delete several nodes and their edges in one undoable command.
+ *
+ * `node_count` may be 0 (pass `nodes`/`contexts` as NULL) to delete only
+ * edges; the call is invalid only when both counts are 0.
+ */
+OAKENGINE_API int oakengine_nodes_delete_many(
+ OakEngineNode *const *nodes, OakEngineNode *const *contexts,
+ int node_count, OakEngineNode *const *edge_outputs,
+ OakEngineNode *const *edge_input_nodes,
+ const char *const *edge_input_ids,
+ const int *edge_input_elements, int edge_count);
+
+/**
+ * @brief oakengine_nodes_delete_many() plus edges to (re)connect AFTER the
+ * deletion, still inside the same single undoable command.
+ *
+ * The reconnect edges are applied after the nodes are gone, so they may
+ * target inputs that were occupied by the deleted nodes (effect-bypass
+ * rewiring in the parameter editor). Redo order: delete, then reconnect;
+ * undo order is the reverse.
+ */
+OAKENGINE_API int oakengine_nodes_delete_many_ex(
+ OakEngineNode *const *nodes, OakEngineNode *const *contexts,
+ int node_count, OakEngineNode *const *edge_outputs,
+ OakEngineNode *const *edge_input_nodes,
+ const char *const *edge_input_ids,
+ const int *edge_input_elements, int edge_count,
+ OakEngineNode *const *reconnect_outputs,
+ OakEngineNode *const *reconnect_input_nodes,
+ const char *const *reconnect_input_ids,
+ const int *reconnect_input_elements, int reconnect_count);
+
+/* ---- Keyframe best type at time ------------------------------------------- */
+
+/**
+ * @brief The best easing type for a keyframe at the given time (used by
+ * the panel to determine the default type when adding keys).
+ */
+OAKENGINE_API int oakengine_node_keyframe_best_type_at_time(
+ const OakEngineNode *self, const char *input_id, int element,
+ int64_t time_ts, int track, int default_type);
+
+/* ---- Handle-based keyframe API -------------------------------------------- */
+
+/**
+ * @brief Number of keyframe tracks on the input (-1 for all).
+ */
+OAKENGINE_API int oakengine_node_keyframe_track_count(
+ const OakEngineNode *self, const char *input_id, int element);
+
+/**
+ * @brief Number of keyframes on a specific track.
+ */
+OAKENGINE_API int oakengine_node_keyframe_count_on_track(
+ const OakEngineNode *self, const char *input_id, int element,
+ int track);
+
+/**
+ * @brief Toggle keyframing on/off at a time (add/remove one key).
+ */
+OAKENGINE_API int oakengine_node_keyframes_toggle_at_time(
+ OakEngineNode *self, const char *input_id, int element,
+ int64_t time_ts, int track, int on, const char *undo_name);
+
+/**
+ * @brief 1 if a keyframe exists at the given time on the given track.
+ */
+OAKENGINE_API int oakengine_node_has_keyframe_at_time(
+ const OakEngineNode *self, const char *input_id, int element,
+ int64_t time_ts, int track);
+
+/**
+ * @brief The earliest keyframe time on the input. Returns 1 if found,
+ * 0 if no keyframes (and the output rational is set).
+ */
+OAKENGINE_API int oakengine_node_keyframe_earliest_time(
+ const OakEngineNode *self, const char *input_id, int element,
+ int64_t *num, int64_t *den);
+
+/**
+ * @brief The latest keyframe time on the input. Returns 1 if found,
+ * 0 if no keyframes.
+ */
+OAKENGINE_API int oakengine_node_keyframe_latest_time(
+ const OakEngineNode *self, const char *input_id, int element,
+ int64_t *num, int64_t *den);
+
+/**
+ * @brief The closest keyframe time before the given time.
+ * Returns 1 if found, 0 if none.
+ */
+OAKENGINE_API int oakengine_node_keyframe_closest_time_before(
+ const OakEngineNode *self, const char *input_id, int element,
+ int64_t time_ts, int track, int64_t *num, int64_t *den);
+
+/**
+ * @brief The closest keyframe time after the given time.
+ * Returns 1 if found, 0 if none.
+ */
+OAKENGINE_API int oakengine_node_keyframe_closest_time_after(
+ const OakEngineNode *self, const char *input_id, int element,
+ int64_t time_ts, int track, int64_t *num, int64_t *den);
+
+/**
+ * @brief Borrowed handle of the keyframe at the given on-track index,
+ * or NULL.
+ */
+OAKENGINE_API OakEngineKeyframe *oakengine_node_keyframe_handle_on_track(
+ const OakEngineNode *self, const char *input_id, int element,
+ int track, int index);
+
+/**
+ * @brief Borrowed handle of the keyframe at the given time on a track,
+ * or NULL.
+ */
+OAKENGINE_API OakEngineKeyframe *oakengine_node_keyframe_handle_at_time(
+ const OakEngineNode *self, const char *input_id, int element,
+ int track, int64_t time_ts, int track_for_time);
+
+/**
+ * @brief Fill an array with keyframe handles at a given time. Returns
+ * the number filled.
+ */
+OAKENGINE_API int oakengine_node_keyframes_at_time(
+ const OakEngineNode *self, const char *input_id, int element,
+ int64_t time_ts, int track, OakEngineKeyframe **out_handles,
+ int max_handles);
+
+/**
+ * @brief Enable or disable keyframing on an input for a given element
+ * (undoable). If enabling, one default-type key per track is added.
+ */
+OAKENGINE_API int oakengine_node_set_input_keyframing(
+ OakEngineNode *self, const char *input_id, int element,
+ int keyframing, int track, int enable_all_tracks,
+ const char *undo_name);
+
+/**
+ * @brief Create a NodeParamSetKeyframingCommand as an opaque command pointer.
+ */
+OAKENGINE_API void *oakengine_node_set_input_keyframing_command(
+ OakEngineNode *self, const char *input_id, int element, int keyframing);
+
+/**
+ * @brief Paste detached keyframes onto the input's track (undoable,
+ * ONE command).
+ */
+OAKENGINE_API int oakengine_node_keyframes_paste(
+ OakEngineNode *self, OakEngineKeyframe *const *keyframes,
+ int count, const char *undo_name);
+
+/* ---- OakEngineKeyframe accessors ------------------------------------------ */
+
+/**
+ * @brief The keyframe's time as a rational.
+ */
+OAKENGINE_API int oakengine_keyframe_get_time(
+ const OakEngineKeyframe *self, int64_t *num, int64_t *den);
+
+/**
+ * @brief The input id that owns this keyframe (buf/size).
+ */
+OAKENGINE_API int oakengine_keyframe_get_input_id(
+ const OakEngineKeyframe *self, char *buf, int buf_size);
+
+/**
+ * @brief The track this keyframe belongs to.
+ */
+OAKENGINE_API int oakengine_keyframe_get_track(
+ const OakEngineKeyframe *self);
+
+/**
+ * @brief The element this keyframe belongs to.
+ */
+OAKENGINE_API int oakengine_keyframe_get_element(
+ const OakEngineKeyframe *self);
+
+/**
+ * @brief The node that owns this keyframe.
+ */
+OAKENGINE_API OakEngineNode *oakengine_keyframe_get_node(
+ const OakEngineKeyframe *self);
+
+/**
+ * @brief The easing type of the keyframe (0=linear, 1=bezier, 2=hold;
+ * -1 on NULL).
+ */
+OAKENGINE_API int oakengine_keyframe_get_type(
+ const OakEngineKeyframe *self);
+
+/**
+ * @brief The default easing type for a new keyframe.
+ */
+OAKENGINE_API int oakengine_keyframe_default_type(void);
+
+/**
+ * @brief The opposing bezier handle type (0=k_in_handle ⇄ 1=k_out_handle).
+ */
+OAKENGINE_API int oakengine_keyframe_opposing_bezier_type(int type);
+
+/**
+ * @brief The value of the keyframe on its track.
+ */
+OAKENGINE_API int oakengine_keyframe_get_value(
+ const OakEngineKeyframe *self, oak_node_value *out);
+
+/**
+ * @brief 1 if there is a sibling keyframe at the given time on a different
+ * track of the same input.
+ */
+OAKENGINE_API int oakengine_keyframe_has_sibling_at_time(
+ const OakEngineKeyframe *self, int64_t time_ts, int track);
+
+/**
+ * @brief Live-set a bezier control point (no undo).
+ */
+OAKENGINE_API int oakengine_keyframe_set_bezier_point_live(
+ OakEngineKeyframe *self, int point_index, double x, double y);
+
+/**
+ * @brief Read a bezier control point (0 = in-handle, 1 = out-handle).
+ */
+OAKENGINE_API int oakengine_keyframe_get_bezier_point(
+ const OakEngineKeyframe *self, int point_index, double *x,
+ double *y);
+
+/**
+ * @brief Read a bezier control point that is valid (returns the
+ * point or the identity point for non-bezier keyframes).
+ */
+OAKENGINE_API int oakengine_keyframe_get_valid_bezier_point(
+ const OakEngineKeyframe *self, int point_index, double *x,
+ double *y);
+
+/**
+ * @brief Live-set the value of a keyframe (no undo).
+ */
+OAKENGINE_API int oakengine_keyframe_set_value_live(
+ OakEngineKeyframe *self, const oak_node_value *value);
+
+/**
+ * @brief Live-set the time of a keyframe (no undo).
+ */
+OAKENGINE_API int oakengine_keyframe_set_time_live(
+ OakEngineKeyframe *self, int64_t num, int64_t den);
+
+/**
+ * @brief Remove several keyframes in one undoable command.
+ */
+OAKENGINE_API int oakengine_keyframes_remove_many(
+ OakEngineKeyframe *const *keyframes, int count,
+ const char *undo_name);
+
+/**
+ * @brief Create a detached keyframe (not yet on any track).
+ */
+OAKENGINE_API OakEngineKeyframe *oakengine_keyframe_create(
+ OakEngineNode *node, const char *input_id, int element,
+ int track, int64_t time_ts, int type,
+ const oak_node_value *value, int64_t duration_ts);
+
+/**
+ * @brief Dispose a detached keyframe (no-op on NULL).
+ */
+OAKENGINE_API void oakengine_keyframe_dispose(
+ OakEngineKeyframe *keyframe);
+
+/* ---- Input dragger -------------------------------------------------------- */
+
+/**
+ * @brief Create an input dragger for live-drag of a keyframe value.
+ */
+OAKENGINE_API OakEngineNodeDragger *oakengine_dragger_create(
+ OakEngineNode *node, const char *input_id, int element,
+ int track);
+
+/**
+ * @brief Start the drag at the given frame timestamp (creates a keyframe).
+ */
+OAKENGINE_API int oakengine_dragger_start(
+ OakEngineNodeDragger *self, int64_t time_ts, int track,
+ int insert_on_all_tracks);
+
+/**
+ * @brief Drag to a new value (live; no undo).
+ */
+OAKENGINE_API int oakengine_dragger_drag(
+ OakEngineNodeDragger *self, const oak_node_value *value);
+
+/**
+ * @brief End the drag, pushing ONE undoable command.
+ */
+OAKENGINE_API int oakengine_dragger_end(
+ OakEngineNodeDragger *self, const char *undo_name);
+
+/**
+ * @brief 1 if the dragger has been started.
+ */
+OAKENGINE_API int oakengine_dragger_is_started(
+ const OakEngineNodeDragger *self);
+
+/**
+ * @brief Free the dragger (no-op on NULL).
+ */
+OAKENGINE_API void oakengine_dragger_free(
+ OakEngineNodeDragger *self);
+
+/* ---- Node static data and helpers ----------------------------------------- */
+
+/**
+ * @brief Node::k_enabled_input. Static string, never freed.
+ */
+OAKENGINE_API const char *oakengine_node_enabled_input_id(void);
+
+/** @brief VolumeNode::k_samples_input. Static string, never freed. */
+OAKENGINE_API const char *oakengine_volume_samples_input_id(void);
+
+/** @brief TransformDistortNode::k_texture_input. Static string. */
+OAKENGINE_API const char *oakengine_transform_texture_input_id(void);
+
+/** @brief TransitionBlock::k_in_block_input. Static string. */
+OAKENGINE_API const char *oakengine_transition_in_block_input_id(void);
+
+/** @brief TransitionBlock::k_out_block_input. Static string. */
+OAKENGINE_API const char *oakengine_transition_out_block_input_id(void);
+
+/** @brief AudioVisualWaveform::k_maximum_sample_rate as a double. */
+OAKENGINE_API double oakengine_audio_waveform_max_sample_rate(void);
+
+/**
+ * @brief Node::get_category_name() (buf/size convention).
+ * `category_id` is a Node::CategoryID value.
+ */
+OAKENGINE_API int oakengine_node_category_name(int category_id,
+ char *buf, int buf_size);
+
+/**
+ * @brief Create a NodeLinkCommand as an opaque command pointer.
+ * `link` != 0 links the two nodes, 0 unlinks them.
+ */
+OAKENGINE_API void *oakengine_node_link_command(OakEngineNode *a,
+ OakEngineNode *b, int link);
+
+/**
+ * @brief Node::copy_node_in_graph(). Returns the copy as a borrowed
+ * OakEngineNode*, or NULL on failure. The copy is added to `command`
+ * (a MultiUndoCommand*) when non-NULL; when NULL a standalone command
+ * is pushed.
+ */
+OAKENGINE_API OakEngineNode *oakengine_node_copy_in_graph(
+ OakEngineNode *node, void *command);
+
+/**
+ * @brief Node::copy_dependency_graph(). `nodes` and `copies` are
+ * parallel arrays of the same length; the function connects the copies
+ * the same way the originals are connected. `command` is a
+ * MultiUndoCommand* (may be NULL for direct application).
+ */
+OAKENGINE_API int oakengine_node_copy_dependency_graph(
+ OakEngineNode *const *nodes, OakEngineNode *const *copies, int count,
+ void *command);
+
+/**
+ * @brief Node::get_connect_command_string() (buf/size convention).
+ * Returns a human-readable description of connecting `output` to the
+ * input `input_id`/`element` of `input_node`.
+ */
+OAKENGINE_API int oakengine_node_connect_command_string(
+ OakEngineNode *output, OakEngineNode *input_node,
+ const char *input_id, int element, char *buf, int buf_size);
+
+/**
+ * @brief Node::transform_time_to(). Transforms a time range through the
+ * node graph from `from` to `to`. Returns the transformed range as
+ * rational seconds (in_num/in_den, out_num/out_den).
+ */
+OAKENGINE_API int oakengine_node_transform_time_to(
+ OakEngineNode *from, OakEngineNode *to, int direction,
+ int path_index, int64_t in_num, int64_t in_den,
+ int64_t out_num, int64_t out_den,
+ int64_t *result_in_num, int64_t *result_in_den,
+ int64_t *result_out_num, int64_t *result_out_den);
+
+/* ---- NodeValue static methods (F class: 4 symbols) ----------------------- */
+
+/**
+ * @brief NodeValue::get_number_of_keyframe_tracks(type) using C enum.
+ *
+ * `c_type` is an oak_node_value_type value (NOT olive::NodeValue::Type
+ * enum ordinal). Returns the number of keyframe tracks for the type:
+ * 1 for scalar types, 2/3/4/6 for VEC2/VEC3/VEC4/COLOR/BEZIER.
+ */
+OAKENGINE_API int oakengine_node_value_keyframe_track_count(int c_type);
+
+/**
+ * @brief NodeValue::get_pretty_data_type_name(type) into buf (buf/size).
+ *
+ * `c_type` is an oak_node_value_type value. Returns the would-be string
+ * length (excluding NUL), or -1 for unknown type.
+ */
+OAKENGINE_API int oakengine_node_value_pretty_type_name(int c_type,
+ char *buf, int buf_size);
+
+/**
+ * @brief NodeValue::split_normal_value_into_track_values() into a
+ * pre-allocated array.
+ *
+ * `c_type` is an oak_node_value_type. `normal` is the input value.
+ * `tracks_out` must hold at least `track_count` oak_node_value entries
+ * (caller allocates; get track_count first via
+ * oakengine_node_value_keyframe_track_count()). Returns OAKENGINE_OK
+ * or OAKENGINE_E_INVALID.
+ *
+ * For non-split types (VEC2/3/4/COLOR/BEZIER), the value is split into
+ * per-component tracks. For scalar types, tracks_out[0] gets the value.
+ */
+OAKENGINE_API int oakengine_node_value_split_to_tracks(int c_type,
+ const oak_node_value *normal, oak_node_value *tracks_out, int track_count);
+
+/**
+ * @brief NodeValue::combine_track_values_into_normal_value() — split
+ * reverse.
+ *
+ * `c_type` is an oak_node_value_type. `tracks` must have at least
+ * `track_count` entries (from oakengine_node_value_keyframe_track_count).
+ * Returns OAKENGINE_OK or OAKENGINE_E_INVALID.
+ */
+OAKENGINE_API int oakengine_node_value_combine_tracks(int c_type,
+ const oak_node_value *tracks, int track_count, oak_node_value *normal_out);
+
#ifdef __cplusplus
}
#endif
+/* Qt meta-type support: these opaque C handles are used as signal/slot
+ * parameters across the C ABI boundary. Declaring them as opaque pointers
+ * lets QMetaType store them (queued connections, QSignalSpy, QVariant). */
+#ifdef __cplusplus
+#include
+Q_DECLARE_OPAQUE_POINTER(OakEngineNode *)
+Q_DECLARE_OPAQUE_POINTER(OakEngineKeyframe *)
+Q_DECLARE_OPAQUE_POINTER(OakEngineNodeDragger *)
+#endif
+
#endif /* OAKENGINE_NODE_H */
diff --git a/engine/include/oakengine/plugin.h b/engine/include/oakengine/plugin.h
new file mode 100644
index 000000000..5c28ef794
--- /dev/null
+++ b/engine/include/oakengine/plugin.h
@@ -0,0 +1,74 @@
+/***
+
+ 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 .
+
+***/
+
+#ifndef OAKENGINE_PLUGIN_H
+#define OAKENGINE_PLUGIN_H
+
+#include "export.h"
+#include "init.h"
+#include "node.h"
+
+#ifdef __cplusplus
+extern "C" {
+#endif
+
+/**
+ * @file plugin.h
+ * @brief C ABI for plugin support (active viewer, progress reporter, loading)
+ */
+
+/* ---- Active viewer provider -------------------------------------------- */
+
+/** @brief Returns the currently active viewer node (or NULL). */
+typedef OakEngineNode *(*oakengine_plugin_active_viewer_fn)(void *userdata);
+
+OAKENGINE_API int oakengine_plugin_set_active_viewer_provider(
+ oakengine_plugin_active_viewer_fn fn, void *userdata);
+
+/* ---- Progress reporter factory ----------------------------------------- */
+
+typedef void *(*oakengine_plugin_reporter_create_fn)(
+ const char *message, const char *title, void *userdata);
+typedef void (*oakengine_plugin_reporter_destroy_fn)(
+ void *reporter, void *userdata);
+typedef int (*oakengine_plugin_reporter_is_cancelled_fn)(
+ void *reporter, void *userdata);
+typedef void (*oakengine_plugin_reporter_set_progress_fn)(
+ void *reporter, double progress, void *userdata);
+
+OAKENGINE_API int oakengine_plugin_set_progress_reporter_factory(
+ oakengine_plugin_reporter_create_fn create,
+ oakengine_plugin_reporter_destroy_fn destroy,
+ oakengine_plugin_reporter_is_cancelled_fn is_cancelled,
+ oakengine_plugin_reporter_set_progress_fn set_progress,
+ void *userdata);
+
+/* ---- Plugin loading and interaction ------------------------------------ */
+
+OAKENGINE_API int oakengine_plugin_load_plugins(const char *path);
+
+OAKENGINE_API int oakengine_plugin_node_push_button_clicked(
+ OakEngineNode *node, const char *button_id);
+
+#ifdef __cplusplus
+}
+#endif
+
+#endif /* OAKENGINE_PLUGIN_H */
diff --git a/engine/include/oakengine/preview.h b/engine/include/oakengine/preview.h
index 7f4d269fc..237e56fa9 100644
--- a/engine/include/oakengine/preview.h
+++ b/engine/include/oakengine/preview.h
@@ -62,6 +62,24 @@ extern "C" {
#define OAKENGINE_LOOP_MODE_LOOP 1 /**< Repeat the clip (olive k_loop_mode_loop). */
#define OAKENGINE_LOOP_MODE_CLAMP 2 /**< Hold first/last frame (olive k_loop_mode_clamp). */
+/**
+ * @brief Opaque preview request handle (an active render ticket for
+ * single-frame or audio-range preview).
+ */
+typedef struct OakEnginePreviewRequest OakEnginePreviewRequest;
+
+/**
+ * @brief POD for a single video frame from a preview request
+ * (borrowed data, valid until the request is freed).
+ */
+typedef struct oak_playback_frame {
+ int width;
+ int height;
+ int format; /**< olive::PixelFormat::Format value. */
+ const void *data; /**< Planar data pointer (first plane). */
+ int linesize; /**< Bytes per row of the first plane. */
+} oak_playback_frame;
+
/**
* @brief Human-readable reason for the last failed preview call on this
* thread (buf/size convention).
@@ -118,6 +136,108 @@ OAKENGINE_API int oakengine_preview_get_waveform_summary(
OakEngineFootage *footage, int channel, int64_t start_ts,
int64_t end_ts, double *min_vals, double *max_vals, int count);
+/* ---- R4: waveform, audio levels, cacher, preview requests ------------------ */
+
+/** @brief Maximum sample rate for waveform generation. > 0. */
+OAKENGINE_API int oakengine_waveform_max_sample_rate(void);
+
+/**
+ * @brief Analyze audio levels (linear RMS) from raw float sample data.
+ * `data` is an array of `channels` float pointers, each with `count` samples.
+ * Writes RMS values into `levels` (one per channel). Returns OAKENGINE_OK
+ * or OAKENGINE_E_INVALID on NULL/bad arguments.
+ */
+OAKENGINE_API int oakengine_audio_analyze_levels(const float *const *data,
+ int channels, int64_t count,
+ double *levels);
+
+/**
+ * @brief Set the preview cacher's playhead position (num/den seconds).
+ * Returns OAKENGINE_E_STATE when the cacher is not available.
+ */
+OAKENGINE_API int oakengine_preview_cacher_set_playhead(int64_t num,
+ int64_t den);
+
+/**
+ * @brief Pause or resume thumbnail generation in the cacher.
+ * Returns OAKENGINE_E_STATE when the cacher is not available.
+ */
+OAKENGINE_API int oakengine_preview_cacher_set_thumbnails_paused(int paused);
+
+/**
+ * @brief Clear pending single-frame render requests from the cacher.
+ * Returns OAKENGINE_E_STATE when the cacher is not available.
+ */
+OAKENGINE_API int
+oakengine_preview_cacher_clear_single_frame_renders(int only_finished);
+
+/**
+ * @brief Force the cacher to cache a range (num/den seconds in/out).
+ * Returns OAKENGINE_E_INVALID on NULL node.
+ */
+OAKENGINE_API int oakengine_preview_cacher_force_cache_range(
+ OakEngineNode *node, int64_t in_num, int64_t in_den, int64_t out_num,
+ int64_t out_den);
+
+/**
+ * @brief Request a single video frame at (num/den) seconds from `viewer`.
+ * Returns a request handle (caller owns it, must free) or NULL on failure.
+ */
+OAKENGINE_API OakEnginePreviewRequest *
+oakengine_preview_request_single_frame(OakEngineNode *viewer, int64_t num,
+ int64_t den, int dry);
+
+/**
+ * @brief Request an audio range (num/den seconds in/out) from `viewer`.
+ * Returns a request handle (caller owns it, must free) or NULL on failure.
+ */
+OAKENGINE_API OakEnginePreviewRequest *
+oakengine_preview_request_audio_range(OakEngineNode *viewer, int64_t in_num,
+ int64_t in_den, int64_t out_num,
+ int64_t out_den);
+
+/** @brief 1 if the request is done, 0 otherwise. 0 on NULL. */
+OAKENGINE_API int oakengine_preview_request_is_done(
+ const OakEnginePreviewRequest *req);
+
+/** @brief 1 if the request has a result, 0 otherwise. 0 on NULL. */
+OAKENGINE_API int oakengine_preview_request_has_result(
+ const OakEnginePreviewRequest *req);
+
+/** @brief Set a finished callback (called when the ticket completes).
+ * `callback` receives `user_data`. Returns OAKENGINE_E_INVALID on NULL
+ * request. */
+OAKENGINE_API int oakengine_preview_request_set_finished_callback(
+ OakEnginePreviewRequest *req, void (*callback)(void *),
+ void *user_data);
+
+/** @brief Copy the frame data into `out`. Returns OAKENGINE_OK or
+ * OAKENGINE_E_INVALID when the request has no video frame result. */
+OAKENGINE_API int oakengine_preview_request_get_frame(
+ OakEnginePreviewRequest *req, oak_playback_frame *out);
+
+/** @brief Number of audio channels in the result, or 0 if none. */
+OAKENGINE_API int oakengine_preview_request_get_audio_channel_count(
+ const OakEnginePreviewRequest *req);
+
+/** @brief Sample rate of the audio result, or 0 if none. */
+OAKENGINE_API int oakengine_preview_request_get_audio_sample_rate(
+ const OakEnginePreviewRequest *req);
+
+/**
+ * @brief Get audio sample data from the result.
+ * `channel` is the 0-based channel index. Writes up to `max_samples` float
+ * values into `samples`. Returns the number of samples written, or
+ * OAKENGINE_E_INVALID on bad arguments.
+ */
+OAKENGINE_API int oakengine_preview_request_get_audio_samples(
+ OakEnginePreviewRequest *req, int channel, const float *samples,
+ int max_samples);
+
+/** @brief Free a preview request handle (NULL-safe). */
+OAKENGINE_API void oakengine_preview_request_free(
+ OakEnginePreviewRequest *req);
+
#ifdef __cplusplus
}
#endif
diff --git a/engine/include/oakengine/project.h b/engine/include/oakengine/project.h
index 758e2d7ef..f38c3eec9 100644
--- a/engine/include/oakengine/project.h
+++ b/engine/include/oakengine/project.h
@@ -24,6 +24,12 @@
#include "export.h"
#include "init.h"
+/* Forward declarations from node.h (included by callers in either order). */
+typedef struct OakEngineNode OakEngineNode;
+
+/* Forward declaration for playback cache from viewer.h. */
+typedef struct OakEnginePlaybackCache OakEnginePlaybackCache;
+
#ifdef __cplusplus
extern "C" {
#endif
@@ -195,6 +201,127 @@ oakengine_project_sequence_count(const OakEngineProject *self);
OAKENGINE_API OakEngineSequence *
oakengine_project_sequence_at(const OakEngineProject *self, int index);
+/* ---- Folder operations ---------------------------------------------------- */
+
+/**
+ * @brief Create a folder node named `name` under `parent` in `project`.
+ * Returns a borrowed handle, or NULL on failure.
+ */
+OAKENGINE_API OakEngineNode *oakengine_folder_create(OakEngineProject *project,
+ OakEngineNode *parent,
+ const char *name);
+
+/**
+ * @brief 1 if `folder` recursively contains `child`, 0 otherwise.
+ * 0 when either handle is NULL or `folder` is not a Folder.
+ */
+OAKENGINE_API int oakengine_folder_has_child_recursive(
+ const OakEngineNode *folder, const OakEngineNode *child);
+
+/**
+ * @brief Index of `child` in `folder`'s direct children, or
+ * OAKENGINE_E_NOT_FOUND. Returns OAKENGINE_E_INVALID when
+ * `folder` is not a Folder node.
+ */
+OAKENGINE_API int oakengine_folder_index_of_child(
+ const OakEngineNode *folder, const OakEngineNode *child);
+
+/**
+ * @brief Static input key string for Folder children (Folder::k_child_input).
+ * Never freed.
+ */
+OAKENGINE_API const char *oakengine_folder_child_input_key(void);
+
+/**
+ * @brief Add `child` to `folder` (undoable). OAKENGINE_E_INVALID when
+ * `folder` is not a Folder node or on NULL args.
+ */
+OAKENGINE_API int oakengine_folder_add_child(OakEngineNode *folder,
+ OakEngineNode *child);
+
+/**
+ * @brief Move `node` from its current folder to `new_folder` (undoable).
+ * Removes the node from its old folder first — a true move, not a copy.
+ * Returns OAKENGINE_OK or a negative error code.
+ */
+OAKENGINE_API int oakengine_folder_move_child(OakEngineNode *node,
+ OakEngineNode *new_folder);
+
+/**
+ * @brief Create a Folder::RemoveElementCommand as an opaque command pointer.
+ * Returns NULL on invalid arguments.
+ */
+OAKENGINE_API void *oakengine_folder_remove_element_command(
+ OakEngineNode *folder, OakEngineNode *child);
+
+/**
+ * @brief Move several nodes into `dest_folder` as ONE undoable command
+ * (each node is removed from its old folder, then added to `dest_folder`).
+ * Nodes already directly inside `dest_folder` are skipped. `undo_name`
+ * may be NULL. Returns OAKENGINE_OK or a negative error code.
+ */
+OAKENGINE_API int oakengine_folder_move_children(
+ OakEngineNode *const *nodes, int count, OakEngineNode *dest_folder,
+ const char *undo_name);
+
+/* ---- Project extras ------------------------------------------------------- */
+
+/** @brief Root folder node of the project (Project::root()). */
+OAKENGINE_API OakEngineNode *oakengine_project_root(OakEngineProject *self);
+
+/** @brief Display name for the project that is safe for window titles
+ * (Project::pretty_filename()). buf/size convention. */
+OAKENGINE_API int oakengine_project_pretty_filename(const OakEngineProject *self,
+ char *buf, int buf_size);
+
+/** @brief Set the project's filename (Project::set_filename()).
+ * Returns OAKENGINE_OK or OAKENGINE_E_INVALID on NULL. */
+OAKENGINE_API int oakengine_project_set_filename(OakEngineProject *self,
+ const char *path);
+
+/** @brief The project's default cache directory (Project::cache_path()).
+ * buf/size convention. */
+OAKENGINE_API int oakengine_project_cache_path(const OakEngineProject *self,
+ char *buf, int buf_size);
+
+/** @brief The project's alongside cache directory
+ * (Project::cache_alongside_path()). buf/size convention. */
+OAKENGINE_API int oakengine_project_cache_alongside_path(
+ const OakEngineProject *self, char *buf, int buf_size);
+
+/** @brief Set a custom cache directory path (Project::set_custom_cache_path()).
+ * NULL clears it. */
+OAKENGINE_API int oakengine_project_set_custom_cache_path(
+ OakEngineProject *self, const char *path);
+
+/** @brief Get the custom cache directory path, or "" when none is set.
+ * buf/size convention; returns 0 when no custom path is set. */
+OAKENGINE_API int oakengine_project_get_custom_cache_path(
+ const OakEngineProject *self, char *buf, int buf_size);
+
+/** @brief Cache location setting enum value
+ * (Project::get_cache_location_setting()). Returns < 0 on NULL. */
+OAKENGINE_API int oakengine_project_get_cache_location_setting(
+ const OakEngineProject *self);
+
+/** @brief Static MIME type string for project items (Project::item_mime_type()).
+ * Never freed. */
+OAKENGINE_API const char *oakengine_project_item_mime_type(void);
+
+/** @brief Resolve a project node to its owning OakEngineProject
+ * (Project::get_project_from_object()). Returns NULL when the node is
+ * not part of a project or on NULL input. */
+OAKENGINE_API OakEngineProject *
+oakengine_project_from_object(const OakEngineNode *node);
+
+/** @brief Get the project's color reference space name (buf/size). */
+OAKENGINE_API int oakengine_project_get_color_reference_space(
+ const OakEngineProject *self, char *buf, int buf_size);
+
+/** @brief Set the project's color reference space (undoable). */
+OAKENGINE_API int oakengine_project_set_color_reference_space(
+ OakEngineProject *self, const char *colorspace);
+
#ifdef __cplusplus
}
#endif
diff --git a/engine/include/oakengine/proxy.h b/engine/include/oakengine/proxy.h
new file mode 100644
index 000000000..fc569fc65
--- /dev/null
+++ b/engine/include/oakengine/proxy.h
@@ -0,0 +1,131 @@
+/***
+
+ 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 .
+
+***/
+
+#ifndef OAKENGINE_PROXY_H
+#define OAKENGINE_PROXY_H
+
+#include
+
+#include "export.h"
+#include "footage.h"
+#include "init.h"
+
+#ifdef __cplusplus
+extern "C" {
+#endif
+
+/**
+ * @file proxy.h
+ * @brief C ABI for the engine's proxy generation singleton (olive::ProxyManager)
+ *
+ * A thin facade over ProxyManager's instance lifecycle, proxy parameter
+ * configuration, proxy state queries and proxy generation. The opaque task
+ * handle returned in oak_proxy_result::task is a borrowed pointer to the
+ * engine's internal ProxyTask; it is intended only for logging and becomes
+ * invalid when the proxy operation finishes.
+ *
+ * Conventions match the other facade families:
+ * - 0 (OAKENGINE_OK) / negative OAKENGINE_E_* codes.
+ * - String output uses the buf/size convention.
+ * - Booleans are int (1/0).
+ */
+
+#define OAKENGINE_PROXY_STATE_MISSING 0
+#define OAKENGINE_PROXY_STATE_GENERATING 1
+#define OAKENGINE_PROXY_STATE_READY 2
+#define OAKENGINE_PROXY_STATE_FAILED 3
+
+typedef struct oak_proxy_result {
+ int state; /**< OAKENGINE_PROXY_STATE_* */
+ char filename[1024];
+ int64_t task; /**< ProxyTask* as opaque handle, or 0 if none */
+} oak_proxy_result;
+
+/**
+ * @brief Create the ProxyManager singleton.
+ *
+ * Safe to call when the instance already exists (no-op). Returns
+ * OAKENGINE_OK or OAKENGINE_E_FAILED.
+ */
+OAKENGINE_API int oakengine_proxy_create_instance(void);
+
+/**
+ * @brief Destroy the ProxyManager singleton.
+ *
+ * Safe to call when no instance exists (no-op). Returns OAKENGINE_OK.
+ */
+OAKENGINE_API int oakengine_proxy_destroy_instance(void);
+
+/**
+ * @brief Build proxy parameters from the global application config.
+ *
+ * Fills `out` with the configured width/height/divider/version/crf/extension
+ * /preset/include_audio values. Returns OAKENGINE_OK or an error code.
+ */
+OAKENGINE_API int oakengine_proxy_params_from_config(oak_proxy_params *out);
+
+/**
+ * @brief Query the state of a proxy file on disk.
+ *
+ * Returns one of the OAKENGINE_PROXY_STATE_* values, or
+ * OAKENGINE_PROXY_STATE_MISSING if `proxy_filename` is NULL/empty or the
+ * proxy does not exist.
+ */
+OAKENGINE_API int oakengine_proxy_get_state(const char *proxy_filename);
+
+/**
+ * @brief Human-readable string for a proxy state (buf/size convention).
+ *
+ * Returns the string length on success, or a negative OAKENGINE_E_* code for
+ * an unknown state.
+ */
+OAKENGINE_API int oakengine_proxy_state_to_string(int state, char *buf,
+ int buf_size);
+
+/**
+ * @brief Get or start generating a proxy for `source_filename`.
+ *
+ * `cache_path` is the project cache directory. `stream_index` is the source
+ * stream to proxy. `params` are the proxy generation parameters (width/height
+ * etc.). On return `out->state` and `out->filename` describe the proxy; if a
+ * generation task was started, `out->task` is a borrowed opaque handle to it,
+ * otherwise it is 0.
+ */
+OAKENGINE_API int oakengine_proxy_get_or_start(const char *cache_path,
+ const char *source_filename,
+ int stream_index,
+ const oak_proxy_params *params,
+ oak_proxy_result *out);
+
+/**
+ * @brief Get the "working" filename for a proxy file (buf/size convention).
+ *
+ * The working filename is used by the proxy generator while the proxy is being
+ * generated. Returns the string length on success, or a negative
+ * OAKENGINE_E_* code on error.
+ */
+OAKENGINE_API int oakengine_proxy_get_working_filename(const char *proxy_filename,
+ char *buf, int buf_size);
+
+#ifdef __cplusplus
+}
+#endif
+
+#endif /* OAKENGINE_PROXY_H */
diff --git a/engine/include/oakengine/renderer.h b/engine/include/oakengine/renderer.h
index 7aaca40e9..67af56135 100644
--- a/engine/include/oakengine/renderer.h
+++ b/engine/include/oakengine/renderer.h
@@ -87,6 +87,45 @@ typedef struct OakEngineFrame OakEngineFrame;
*/
typedef struct OakEngineAudioBuffer OakEngineAudioBuffer;
+/**
+ * @brief Set aggressive garbage collection on the render manager
+ * (RenderManager::set_aggressive_garbage_collection()). Returns
+ * OAKENGINE_E_STATE when the render manager is not available.
+ */
+OAKENGINE_API int
+oakengine_render_manager_set_aggressive_garbage_collection(int aggressive);
+
+/**
+ * @brief The render backend that was requested (RenderManager::requested_backend()).
+ * Returns 0 (k_open_gl) when the render manager is not available.
+ */
+OAKENGINE_API int oakengine_render_manager_requested_backend(void);
+
+/**
+ * @brief Convert a render backend enum value to a human-readable string
+ * (RenderManager::backend_to_string()). buf/size convention. Returns the
+ * would-be length or a negative error code.
+ */
+OAKENGINE_API int oakengine_render_manager_backend_to_string(int backend,
+ char *buf,
+ int buf_size);
+
+/**
+ * @brief Set the display color processor on the render manager's cacher.
+ * `processor` is a borrowed OakEngineColorProcessor handle (NULL to clear).
+ * Returns OAKENGINE_OK or OAKENGINE_E_STATE.
+ */
+OAKENGINE_API int oakengine_render_cache_set_display_color_processor(
+ void *processor);
+
+/**
+ * @brief Set the multicam node on the render manager's cacher.
+ * `node` is a borrowed OakEngineNode handle (NULL to clear).
+ * Returns OAKENGINE_OK or OAKENGINE_E_STATE.
+ */
+OAKENGINE_API int oakengine_render_cache_set_multicam_node(
+ OakEngineNode *node);
+
/**
* @brief Create a renderer for `seq` producing `width`x`height` frames of
* `pixel_format` at the given frame rate.
diff --git a/engine/include/oakengine/serializer.h b/engine/include/oakengine/serializer.h
new file mode 100644
index 000000000..e12bc1bcd
--- /dev/null
+++ b/engine/include/oakengine/serializer.h
@@ -0,0 +1,254 @@
+/***
+
+ 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 .
+
+***/
+
+#ifndef OAKENGINE_SERIALIZER_H
+#define OAKENGINE_SERIALIZER_H
+
+#include "export.h"
+#include "init.h"
+#include "node.h"
+#include "project.h"
+
+#ifdef __cplusplus
+extern "C" {
+#endif
+
+/**
+ * @file serializer.h
+ * @brief C ABI for project serialization / copy-paste (olive::ProjectSerializer)
+ *
+ * A thin facade over ProjectSerializer's load/save/copy/paste primitives. The
+ * opaque OakEngineClipboard handle bundles a SaveData object (for copy/save)
+ * or the LoadData result of the last paste operation. Clipboard handles are
+ * owned by the caller and must be released with oakengine_clipboard_free().
+ *
+ * Conventions match the other facade families:
+ * - 0 (OAKENGINE_OK) / negative OAKENGINE_E_* codes.
+ * - String output uses the buf/size convention.
+ */
+
+/** @brief Opaque clipboard context. */
+typedef struct OakEngineClipboard OakEngineClipboard;
+
+/** @brief Marker handle (defined in oakengine/timeline.h). */
+typedef struct OakEngineMarker OakEngineMarker;
+
+#define OAKENGINE_CLIPBOARD_PROJECT 0
+#define OAKENGINE_CLIPBOARD_NODES 1
+#define OAKENGINE_CLIPBOARD_CLIPS 2
+#define OAKENGINE_CLIPBOARD_MARKERS 3
+#define OAKENGINE_CLIPBOARD_KEYFRAMES 4
+
+#define OAKENGINE_SERIALIZER_OK 0
+#define OAKENGINE_SERIALIZER_TOO_OLD 1
+#define OAKENGINE_SERIALIZER_TOO_NEW 2
+#define OAKENGINE_SERIALIZER_UNKNOWN_VERSION 3
+#define OAKENGINE_SERIALIZER_FILE_ERROR 4
+#define OAKENGINE_SERIALIZER_XML_ERROR 5
+#define OAKENGINE_SERIALIZER_OVERWRITE_ERROR 6
+#define OAKENGINE_SERIALIZER_NO_DATA 7
+
+/**
+ * @brief Returns 1 if `filename` is a compressed project file, 0 otherwise.
+ */
+OAKENGINE_API int oakengine_serializer_check_compressed(const char *filename);
+
+/**
+ * @brief Create a clipboard context for copy/save operations.
+ *
+ * `load_type` is one of OAKENGINE_CLIPBOARD_*. `project` may be NULL for
+ * load types that do not require it. `filename` may be NULL.
+ */
+OAKENGINE_API OakEngineClipboard *oakengine_clipboard_create(
+ int load_type, OakEngineProject *project, const char *filename);
+
+/**
+ * @brief Set the nodes to serialize on this clipboard.
+ *
+ * Replaces any previously set nodes. `nodes` is an array of `count` borrowed
+ * OakEngineNode handles. Returns OAKENGINE_OK or an error code.
+ */
+OAKENGINE_API int oakengine_clipboard_set_nodes(OakEngineClipboard *cb,
+ const OakEngineNode *const *nodes,
+ int count);
+
+/**
+ * @brief Set the markers to serialize on this clipboard.
+ */
+OAKENGINE_API int oakengine_clipboard_set_markers(
+ OakEngineClipboard *cb, const OakEngineMarker *const *markers, int count);
+
+/**
+ * @brief Set the keyframes to serialize on this clipboard.
+ */
+OAKENGINE_API int oakengine_clipboard_set_keyframes(
+ OakEngineClipboard *cb, const OakEngineKeyframe *const *keyframes,
+ int count);
+
+/**
+ * @brief Set a serialized property attached to a node.
+ *
+ * Properties are free-form (key, value) strings attached to pasted nodes; the
+ * editor uses them for clip in-points/track-refs and node graph positions.
+ * Replaces the value if the same (node, key) pair is set twice. Returns
+ * OAKENGINE_OK or an error code.
+ */
+OAKENGINE_API int oakengine_clipboard_set_property(OakEngineClipboard *cb,
+ OakEngineNode *node,
+ const char *key,
+ const char *value);
+
+/**
+ * @brief Copy this clipboard's data to the system clipboard.
+ *
+ * Returns OAKENGINE_OK or an error code.
+ */
+OAKENGINE_API int oakengine_clipboard_copy(OakEngineClipboard *cb);
+
+/**
+ * @brief Serialize this clipboard's data to XML (buf/size convention).
+ *
+ * Returns the string length on success, or a negative OAKENGINE_E_* code on
+ * error.
+ */
+OAKENGINE_API int oakengine_clipboard_save_to_xml(OakEngineClipboard *cb,
+ char *buf, int buf_size);
+
+/**
+ * @brief Paste data from the system clipboard into `project`.
+ *
+ * `load_type` selects what kind of data to paste. On success `*result_code`
+ * receives OAKENGINE_SERIALIZER_OK and the clipboard is populated with the
+ * paste result (accessible through the oakengine_clipboard_get_loaded_*
+ * accessors). On failure `*result_code` receives one of the
+ * OAKENGINE_SERIALIZER_* error codes and a human-readable detail string is
+ * written to `details_buf` (may be NULL). Returns OAKENGINE_OK on success or
+ * an error code.
+ */
+OAKENGINE_API int oakengine_clipboard_paste(OakEngineClipboard *cb,
+ int load_type,
+ OakEngineProject *project,
+ int *result_code,
+ char *details_buf,
+ int details_buf_size);
+
+/**
+ * @brief Paste data from the system clipboard, invoking `map_fn` for each
+ * original->new node mapping.
+ *
+ * The callback is called once per (original node pointer, pasted node pointer)
+ * pair found in the paste result. The app can use it to build an existing-node
+ * map without exposing C++ containers across the boundary. Returning non-zero
+ * from the callback stops iteration early. Other semantics match
+ * oakengine_clipboard_paste().
+ */
+OAKENGINE_API int oakengine_clipboard_paste_with_map(
+ OakEngineClipboard *cb, int load_type, OakEngineProject *project,
+ int (*map_fn)(OakEngineNode *old, OakEngineNode *new_node, void *userdata),
+ void *userdata, int *result_code, char *details_buf,
+ int details_buf_size);
+
+/**
+ * @brief Destroy a clipboard context.
+ */
+OAKENGINE_API void oakengine_clipboard_free(OakEngineClipboard *cb);
+
+/* ---- Paste result accessors (valid after a successful paste) -------------- */
+
+/**
+ * @brief Number of nodes loaded by the last paste operation.
+ */
+OAKENGINE_API int oakengine_clipboard_get_loaded_node_count(
+ OakEngineClipboard *cb);
+
+/**
+ * @brief Borrowed node handle loaded at `index`.
+ */
+OAKENGINE_API OakEngineNode *oakengine_clipboard_get_loaded_node_at(
+ OakEngineClipboard *cb, int index);
+
+/**
+ * @brief Number of markers loaded by the last paste operation.
+ */
+OAKENGINE_API int oakengine_clipboard_get_loaded_marker_count(
+ OakEngineClipboard *cb);
+
+/**
+ * @brief Borrowed marker handle loaded at `index`.
+ */
+OAKENGINE_API OakEngineMarker *oakengine_clipboard_get_loaded_marker_at(
+ OakEngineClipboard *cb, int index);
+
+/**
+ * @brief Number of keyframes loaded by the last paste operation.
+ */
+OAKENGINE_API int oakengine_clipboard_get_loaded_keyframe_count(
+ OakEngineClipboard *cb);
+
+/**
+ * @brief Borrowed keyframe handle loaded at `index`.
+ */
+OAKENGINE_API OakEngineKeyframe *oakengine_clipboard_get_loaded_keyframe_at(
+ OakEngineClipboard *cb, int index);
+
+/**
+ * @brief Iterate over the serialized properties attached to pasted nodes.
+ *
+ * For each (node, key, value) triple `fn` is called. Returning non-zero stops
+ * iteration early. Returns OAKENGINE_OK or an error code.
+ */
+OAKENGINE_API int oakengine_clipboard_foreach_property(
+ OakEngineClipboard *cb,
+ int (*fn)(OakEngineNode *node, const char *key, const char *value,
+ void *userdata),
+ void *userdata);
+
+/**
+ * @brief Iterate over keyframes loaded by the last paste operation.
+ *
+ * For each keyframe `fn` is called with the node id string it belongs to and
+ * the keyframe handle. Returning non-zero stops iteration early. The app can
+ * group keyframes by node id and route them to the correct destination node.
+ * Returns OAKENGINE_OK or an error code.
+ */
+OAKENGINE_API int oakengine_clipboard_foreach_keyframe(
+ OakEngineClipboard *cb,
+ int (*fn)(const char *node_id, OakEngineKeyframe *keyframe,
+ void *userdata),
+ void *userdata);
+
+/**
+ * @brief Iterate over promised connections from the paste result.
+ *
+ * For each promised edge `fn` is called with the output node, input node,
+ * input id and element index. Returning non-zero stops iteration early.
+ * Returns OAKENGINE_OK or an error code.
+ */
+OAKENGINE_API int oakengine_clipboard_foreach_connection(
+ OakEngineClipboard *cb,
+ int (*fn)(OakEngineNode *output_node, OakEngineNode *input_node,
+ const char *input_id, int element, void *userdata),
+ void *userdata);
+
+#ifdef __cplusplus
+}
+#endif
+
+#endif /* OAKENGINE_SERIALIZER_H */
diff --git a/engine/include/oakengine/sync.h b/engine/include/oakengine/sync.h
new file mode 100644
index 000000000..7a2104fdd
--- /dev/null
+++ b/engine/include/oakengine/sync.h
@@ -0,0 +1,137 @@
+/***
+
+ 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 .
+
+***/
+
+#ifndef OAKENGINE_SYNC_H
+#define OAKENGINE_SYNC_H
+
+#include "export.h"
+#include "timeline.h"
+
+#ifdef __cplusplus
+extern "C" {
+#endif
+
+/**
+ * @file sync.h
+ * @brief C ABI for waveform-based audio synchronization estimation
+ *
+ * Wraps the engine's AudioWaveformSync estimators
+ * (engine/audio/audiowaveformsync.h): the full audio of two clips is
+ * rendered through the renderer family and cross-correlated, yielding
+ * the time offset that aligns the target clip with the reference clip
+ * (the application's timeline "synchronize clips by waveform" feature).
+ *
+ * Both estimators validate first and change nothing on failure (these
+ * are pure measurements). They return OAKENGINE_OK when the correlation
+ * is conclusive (OffsetResult::valid), OAKENGINE_E_STATE when it is
+ * inconclusive -- in that case `out_confidence` is still written so the
+ * caller can compare it against a fallback estimator, and the offset
+ * outputs are set to 0 / the stretch output to 1. OAKENGINE_E_INVALID
+ * covers NULL handles, clips without an on-track range, and sequences
+ * without audio; estimation itself requires the engine initialized
+ * with OAKENGINE_INIT_RENDER (OAKENGINE_E_STATE as well).
+ *
+ * Note this family renders audio freshly per call (no waveform-cache
+ * dependency); the application keeps its cache-envelope path for the
+ * envelope source and uses these functions for the estimation step.
+ * Errors follow the family model: per-thread human-readable reason via
+ * oakengine_sync_last_error().
+ */
+
+/**
+ * @brief Estimate the time offset aligning `target` to `reference`
+ * (AudioWaveformSync::estimate_envelope_offset).
+ *
+ * `out_offset_seconds` receives the signed offset in seconds: the
+ * shift to ADD to the target's timeline position so it aligns with the
+ * reference (negative = move the target earlier -- e.g. the
+ * application's AudioSynchronizer adds it to the reference in-point).
+ * The estimate is quantized to the RMS envelope window
+ * (sample_rate/20 seconds), so callers should expect up to one window
+ * of quantization error. `out_confidence` receives the correlation
+ * confidence in [0, 1] and is always written. Any output pointer may
+ * be NULL. Search bounds mirror the application (sample_rate/20
+ * window, 10-minute maximum offset).
+ */
+OAKENGINE_API int oakengine_sync_estimate_offset(
+ OakEngineSequence *seq, OakEngineClip *reference, OakEngineClip *target,
+ double *out_offset_seconds, double *out_confidence);
+
+/**
+ * @brief Estimate a playback-rate change plus offset aligning `target`
+ * to `reference` (AudioWaveformSync::estimate_stretch_and_offset).
+ *
+ * `out_stretch` receives the rate the target must be played at to
+ * align (> 1 = the target runs slower and must be sped up; the search
+ * range mirrors the application: 0.75..1.34 in 0.005 steps, 30-second
+ * offset radius). `out_offset_seconds` and `out_confidence` behave
+ * like oakengine_sync_estimate_offset(). Any output pointer may be
+ * NULL.
+ */
+OAKENGINE_API int oakengine_sync_estimate_stretch_offset(
+ OakEngineSequence *seq, OakEngineClip *reference, OakEngineClip *target,
+ double *out_stretch, double *out_offset_seconds,
+ double *out_confidence);
+
+/**
+ * @brief Human-readable reason for the last failed sync call on this
+ * thread (buf/size convention). Empty when the last call succeeded.
+ */
+OAKENGINE_API int oakengine_sync_last_error(char *buf, int buf_size);
+
+/* ---- Place by source time / waveform offset (replaces AudioSynchronizer) - */
+
+/** @brief POD for sync placement result (timeline_in rational). */
+typedef struct oak_sync_placement {
+ int64_t timeline_in_num;
+ int64_t timeline_in_den;
+} oak_sync_placement;
+
+/**
+ * @brief Place a clip by source time (AudioSynchronizer::place_by_source_time).
+ *
+ * Computes: timeline_in = anchor_in + (cand_source_start + cand_media_in)
+ * - (ref_source_start + ref_media_in)
+ * Returns OAKENGINE_OK and fills `out`, or OAKENGINE_E_INVALID on NaN input.
+ */
+OAKENGINE_API int oakengine_sync_place_by_source_time(
+ int64_t ref_source_start_num, int64_t ref_source_start_den,
+ int64_t ref_media_in_num, int64_t ref_media_in_den,
+ int64_t cand_source_start_num, int64_t cand_source_start_den,
+ int64_t cand_media_in_num, int64_t cand_media_in_den,
+ int64_t anchor_num, int64_t anchor_den,
+ oak_sync_placement *out);
+
+/**
+ * @brief Place a clip by waveform offset (AudioSynchronizer::place_by_waveform_offset).
+ *
+ * Computes: timeline_in = ref_timeline_in + candidate_offset_samples / sample_rate
+ * Returns OAKENGINE_OK and fills `out`, or OAKENGINE_E_INVALID when sample_rate <= 0.
+ */
+OAKENGINE_API int oakengine_sync_place_by_waveform_offset(
+ int64_t ref_timeline_in_num, int64_t ref_timeline_in_den,
+ int64_t candidate_offset_samples, int sample_rate,
+ oak_sync_placement *out);
+
+#ifdef __cplusplus
+}
+#endif
+
+#endif /* OAKENGINE_SYNC_H */
diff --git a/engine/include/oakengine/task.h b/engine/include/oakengine/task.h
new file mode 100644
index 000000000..9310177fb
--- /dev/null
+++ b/engine/include/oakengine/task.h
@@ -0,0 +1,293 @@
+/***
+
+ 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 .
+
+***/
+
+#ifndef OAKENGINE_TASK_H
+#define OAKENGINE_TASK_H
+
+#include
+
+#include "encoding.h"
+#include "init.h"
+#include "node.h"
+#include "project.h"
+#include "timeline.h"
+
+#ifdef __cplusplus
+extern "C" {
+#endif
+
+/**
+ * @file task.h
+ * @brief C ABI for the engine background-task system (olive::Task /
+ * olive::TaskManager)
+ *
+ * Tasks are engine objects that run a job (project load/save, footage
+ * import, proxy generation, export) on a worker thread. This family lets
+ * C consumers create the concrete task they need, run it synchronously or
+ * hand it to the global TaskManager queue, and observe its lifecycle
+ * through the event mechanism (oakengine/events.h, task family
+ * OAKENGINE_EVENT_TASK_* and manager family
+ * OAKENGINE_EVENT_TASK_MANAGER_*), without ever seeing the C++ classes.
+ *
+ * Conventions (matching oakengine/project.h):
+ * - OakEngineTask is an opaque borrowed/owned pointer to an
+ * olive::Task subclass.
+ * - A task returned by an oakengine_task_create_*() function is OWNED by
+ * the caller until either oakengine_task_manager_add() (the manager
+ * takes ownership and deletes the task when done) or
+ * oakengine_task_free() (the caller deletes it). A task that ran via
+ * oakengine_task_start_sync() is still owned by the caller and must be
+ * released with oakengine_task_free() (or handed to the manager,
+ * though re-running is unusual).
+ * - Once a task was added to the manager its handle must be treated as
+ * borrowed: the manager may delete it at any time after the
+ * OAKENGINE_EVENT_TASK_MANAGER_TASK_REMOVED notification.
+ * - Return codes: 0 (OAKENGINE_OK) on success, negative OAKENGINE_E_*
+ * on failure. String output uses the buf/size convention (return value
+ * is the length that would have been written excluding the NUL; a
+ * negative value is an OAKENGINE_E_* error).
+ */
+
+/**
+ * @brief Opaque task handle (an olive::Task subclass instance).
+ */
+typedef struct OakEngineTask OakEngineTask;
+
+/* ---- Global task manager ------------------------------------------------- */
+
+/**
+ * @brief Borrowed handle of the global TaskManager singleton, for use as
+ * the subscription handle of the OAKENGINE_EVENT_TASK_MANAGER_* events.
+ * Returns NULL when the engine is not initialized.
+ */
+OAKENGINE_API void *oakengine_task_manager_handle(void);
+
+/**
+ * @brief Number of tasks currently known to the manager (running plus
+ * failed-but-kept), or OAKENGINE_E_INVALID when no manager exists.
+ */
+OAKENGINE_API int oakengine_task_manager_count(void);
+
+/**
+ * @brief Borrowed handle of an arbitrary running task (the manager's
+ * "first" task, used by the status bar), or NULL when the queue is empty
+ * or no manager exists.
+ */
+OAKENGINE_API OakEngineTask *oakengine_task_manager_first(void);
+
+/**
+ * @brief Hand `task` to the global manager queue (takes ownership). The
+ * task starts as soon as a worker thread is available.
+ *
+ * @return OAKENGINE_OK, OAKENGINE_E_INVALID for NULL, OAKENGINE_E_STATE
+ * when no manager exists.
+ */
+OAKENGINE_API int oakengine_task_manager_add(OakEngineTask *task);
+
+/**
+ * @brief Ask the manager to cancel `task` (TaskManager::cancel_task
+ * semantics: a running task is signalled; a failed-but-kept task is
+ * removed and deleted).
+ */
+OAKENGINE_API int oakengine_task_manager_cancel(OakEngineTask *task);
+
+/* ---- Task accessors ------------------------------------------------------ */
+
+/**
+ * @brief Title of `task` (buf/size convention).
+ */
+OAKENGINE_API int oakengine_task_title(OakEngineTask *task, char *buf,
+ int buf_size);
+
+/**
+ * @brief Error message of `task` (buf/size convention). Meaningful after a
+ * failed run.
+ */
+OAKENGINE_API int oakengine_task_error(OakEngineTask *task, char *buf,
+ int buf_size);
+
+/**
+ * @brief Start timestamp of `task` (milliseconds since epoch), 0 when the
+ * task never started, OAKENGINE_E_INVALID for NULL.
+ */
+OAKENGINE_API int64_t oakengine_task_start_time(OakEngineTask *task);
+
+/**
+ * @brief 1 when `task` was asked to cancel, 0 otherwise,
+ * OAKENGINE_E_INVALID for NULL.
+ */
+OAKENGINE_API int oakengine_task_is_cancelled(OakEngineTask *task);
+
+/**
+ * @brief Signal `task` to cancel as soon as possible (Task::Cancel).
+ */
+OAKENGINE_API int oakengine_task_cancel(OakEngineTask *task);
+
+/**
+ * @brief Run `task` synchronously on the CALLING thread (Task::start).
+ * Emits the task events on this thread. Ownership stays with the caller.
+ *
+ * @return 1 when the task succeeded, 0 when it failed or was cancelled
+ * (read oakengine_task_error()), OAKENGINE_E_INVALID for NULL.
+ */
+OAKENGINE_API int oakengine_task_start_sync(OakEngineTask *task);
+
+/**
+ * @brief Delete a task that was never added to the manager.
+ */
+OAKENGINE_API int oakengine_task_free(OakEngineTask *task);
+
+/**
+ * @brief Run `task` through the engine's CLI modal progress dialog and return
+ * 1 when it succeeds, 0 when it fails or is cancelled.
+ *
+ * The dialog shows the task's title and progress on the terminal. `parent`
+ * is an optional QObject parent (may be NULL). The task is started
+ * synchronously; the caller retains ownership and must free it with
+ * oakengine_task_free() when done.
+ */
+OAKENGINE_API int oakengine_cli_task_dialog_run(OakEngineTask *task,
+ void *parent_or_NULL);
+
+/* ---- Task creators --------------------------------------------------------
+ *
+ * All creators return an OWNED task (NULL on invalid input). The task is
+ * not started by creation.
+ */
+
+/**
+ * @brief Task that loads an OVE project from `filename`.
+ */
+OAKENGINE_API OakEngineTask *
+oakengine_task_create_project_load(const char *filename);
+
+/**
+ * @brief Task that loads an OpenTimelineIO project from `filename`.
+ * Returns NULL when the engine was built without OTIO support.
+ */
+OAKENGINE_API OakEngineTask *
+oakengine_task_create_project_load_otio(const char *filename);
+
+/**
+ * @brief Task that saves `project` (ProjectSaveTask semantics).
+ *
+ * `use_compression` selects the compressed .ove writer (0 writes the
+ * uncompressed .ovexml form). `override_filename` may be NULL to save to
+ * the project's own filename. `layout` is an opaque
+ * `const olive::SerializedLayoutInfo *` (may be NULL) whose contents are
+ * copied into the saved file.
+ */
+OAKENGINE_API OakEngineTask *oakengine_task_create_project_save(
+ OakEngineProject *project, int use_compression,
+ const char *override_filename, const void *layout);
+
+/**
+ * @brief Task that saves `project` in OpenTimelineIO format. Returns NULL
+ * when the engine was built without OTIO support.
+ */
+OAKENGINE_API OakEngineTask *
+oakengine_task_create_project_save_otio(OakEngineProject *project);
+
+/**
+ * @brief Task that imports `url_count` media files into `folder` (a folder
+ * node of the target project; use oakengine_project_root() for the top
+ * level). The URL array is copied during the call.
+ */
+OAKENGINE_API OakEngineTask *oakengine_task_create_project_import(
+ OakEngineNode *folder, const char **urls, int url_count);
+
+/**
+ * @brief Task that generates the proxy media for `footage` (a footage node
+ * handle, as accepted by oakengine_footage_borrow(); the task keeps the
+ * underlying node).
+ */
+OAKENGINE_API OakEngineTask *
+oakengine_task_create_proxy(OakEngineNode *footage);
+
+/**
+ * @brief Task that renders an export of `sequence` with `params`.
+ *
+ * Takes ownership of `params` (destroyed with the task). Progress is
+ * reported through the OAKENGINE_EVENT_TASK_PROGRESS event; cancelling
+ * the task cancels the engine export render.
+ */
+OAKENGINE_API OakEngineTask *oakengine_task_create_export(
+ OakEngineSequence *sequence, OakEngineEncodingParams *params);
+
+/* ---- Import task results --------------------------------------------------
+ *
+ * Valid on a task created by oakengine_task_create_project_import() after
+ * it ran; all return OAKENGINE_E_INVALID (or 0/NULL) for other tasks.
+ */
+
+/**
+ * @brief Number of files the import task will process (valid right after
+ * creation; 0 means "nothing to import" and the task should be freed
+ * instead of run).
+ */
+OAKENGINE_API int oakengine_task_import_file_count(OakEngineTask *task);
+
+/**
+ * @brief The undo command built by a successful import run as an opaque
+ * `olive::MultiUndoCommand *` (NULL before the run, after a cancelled
+ * run, or on a second call). Ownership is DETACHED from the task and
+ * passes to the caller: push it with oakengine_undo_push() or delete it.
+ */
+OAKENGINE_API void *oakengine_task_import_get_command(OakEngineTask *task);
+
+/**
+ * @brief Number of footage items a successful import run created.
+ */
+OAKENGINE_API int oakengine_task_import_footage_count(OakEngineTask *task);
+
+/**
+ * @brief Borrowed node handle of the imported footage item at `index`
+ * (NULL when out of range).
+ */
+OAKENGINE_API OakEngineNode *
+oakengine_task_import_footage_at(OakEngineTask *task, int index);
+
+/**
+ * @brief Number of files the import task rejected.
+ */
+OAKENGINE_API int
+oakengine_task_import_invalid_files_count(OakEngineTask *task);
+
+/**
+ * @brief Rejected file path at `index` (buf/size convention).
+ */
+OAKENGINE_API int oakengine_task_import_invalid_file_at(OakEngineTask *task,
+ int index, char *buf,
+ int buf_size);
+
+/* ---- Save task results ---------------------------------------------------- */
+
+/**
+ * @brief Borrowed handle of the project a save task wrote (NULL for other
+ * tasks).
+ */
+OAKENGINE_API OakEngineProject *
+oakengine_task_save_get_project(OakEngineTask *task);
+
+#ifdef __cplusplus
+}
+#endif
+
+#endif /* OAKENGINE_TASK_H */
diff --git a/engine/include/oakengine/timeline.h b/engine/include/oakengine/timeline.h
index 559e8b7f4..e09303d5d 100644
--- a/engine/include/oakengine/timeline.h
+++ b/engine/include/oakengine/timeline.h
@@ -301,6 +301,48 @@ OAKENGINE_API int oakengine_sequence_marker_at(const OakEngineSequence *self,
*/
typedef struct OakEngineClip OakEngineClip;
+/**
+ * @brief Opaque marker list handle (olive::TimelineMarkerList).
+ *
+ * Borrowed from oakengine_viewer_get_marker_list(). Invalidated when the
+ * owning viewer is freed.
+ */
+typedef struct OakEngineMarkerList OakEngineMarkerList;
+
+/**
+ * @brief Opaque marker handle (olive::TimelineMarker).
+ *
+ * Borrowed from oakengine_marker_list_at() / oakengine_marker_list_marker_at_time().
+ * Invalidated when the owning project is freed.
+ */
+typedef struct OakEngineMarker OakEngineMarker;
+
+/**
+ * @brief Opaque workarea handle (olive::TimelineWorkArea).
+ *
+ * Borrowed from oakengine_viewer_get_workarea_handle() or created standalone
+ * with oakengine_workarea_create(). Must be freed with oakengine_workarea_free()
+ * when created standalone; borrowed handles are invalidated with their owner.
+ */
+typedef struct OakEngineWorkarea OakEngineWorkarea;
+
+/**
+ * @brief Opaque track handle (olive::Track).
+ *
+ * Borrowed from oakengine_sequence_track_at(). Invalidated when the owning
+ * sequence is freed.
+ */
+typedef struct OakEngineTrack OakEngineTrack;
+
+/**
+ * @brief Opaque block handle (olive::Block).
+ *
+ * A generic block on a track (ClipBlock, GapBlock, TransitionBlock, etc).
+ * Borrowed from events or cast from OakEngineClip* / OakEngineTrack*. The
+ * handle is invalidated when the owning project is freed.
+ */
+typedef struct OakEngineBlock OakEngineBlock;
+
/**
* @brief Human-readable reason for the last failed editing call on this
* thread (buf/size convention). Editing calls return NULL or a negative
@@ -322,6 +364,41 @@ OAKENGINE_API int oakengine_sequence_last_error(char *buf, int buf_size);
OAKENGINE_API int oakengine_sequence_add_track(OakEngineSequence *self,
int track_type);
+/**
+ * @brief Create a TimelineAddTrackCommand as an opaque command pointer without
+ * executing or pushing it. If `out_track` is non-NULL, it receives a borrowed
+ * handle to the track that the command will create on redo.
+ */
+OAKENGINE_API void *oakengine_sequence_add_track_command(
+ OakEngineSequence *self, int track_type, int auto_merge,
+ OakEngineTrack **out_track);
+
+/** Movement modes for ripple/trim commands (mirror olive::Timeline::MovementMode). */
+#define OAKENGINE_MOVEMENT_MODE_NONE 0
+#define OAKENGINE_MOVEMENT_MODE_MOVE 1
+#define OAKENGINE_MOVEMENT_MODE_TRIM_IN 2
+#define OAKENGINE_MOVEMENT_MODE_TRIM_OUT 3
+
+/**
+ * @brief One entry in a TrackListRippleToolCommand hash: the track to ripple,
+ * the block being moved, and whether a gap should be appended after it.
+ */
+typedef struct oakengine_ripple_info {
+ OakEngineTrack *track;
+ OakEngineBlock *block;
+ int append_gap;
+} oakengine_ripple_info;
+
+/**
+ * @brief Create a TrackListRippleToolCommand as an opaque command pointer.
+ * `infos` holds one entry per affected track; `movement` is a rational offset
+ * in seconds. Returns NULL on invalid arguments.
+ */
+OAKENGINE_API void *oakengine_sequence_ripple_tracks_command(
+ OakEngineSequence *self, int track_type,
+ const oakengine_ripple_info *infos, int info_count,
+ int64_t movement_num, int64_t movement_den, int movement_mode);
+
/**
* @brief Place a clip of `footage` on a track (undoable).
*
@@ -374,6 +451,15 @@ OAKENGINE_API int oakengine_clip_get_range(const OakEngineClip *self,
int64_t *in, int64_t *out,
int64_t *media_in);
+/**
+ * @brief The sequence that owns the clip's track.
+ *
+ * Returns a borrowed handle (the clip's track's parent sequence) or NULL if
+ * the clip is not on a track.
+ */
+OAKENGINE_API OakEngineSequence *oakengine_clip_get_sequence(
+ const OakEngineClip *self);
+
/* ---- Editing primitives, round 2: split / ripple delete / trim / move ----
*
* All four are undoable like the other editing primitives and report
@@ -712,6 +798,384 @@ OAKENGINE_API int oakengine_sequence_marker_rename(OakEngineSequence *seq,
int64_t time_ts,
const char *name);
+/* ---- Marker handle family ----------------------------------------------------
+ *
+ * Marker list and individual marker operations on opaque handles. The list
+ * is obtained from oakengine_viewer_get_marker_list() (declared in viewer.h).
+ * These functions operate on the handle level rather than through the sequence,
+ * for fine-grained undo/redo and direct marker manipulation.
+ *
+ * All times are rational seconds (numerator/denominator pairs) matching the
+ * engine's internal time representation.
+ */
+
+/** @brief Number of markers in the list. 0 on a NULL handle. */
+OAKENGINE_API int oakengine_marker_list_count(const OakEngineMarkerList *list);
+
+/** @brief Add a marker with the given rational time range, name, and color.
+ * Returns OAKENGINE_OK or OAKENGINE_E_INVALID. */
+OAKENGINE_API int oakengine_marker_list_add(OakEngineMarkerList *list,
+ int64_t in_num, int64_t in_den,
+ int64_t out_num, int64_t out_den,
+ const char *name, int color);
+
+/**
+ * @brief Create a detached marker (not yet added to any list).
+ *
+ * The returned handle can be passed to MarkerPropertiesDialog, then either
+ * added with oakengine_marker_list_add_existing() or freed with
+ * oakengine_marker_free().
+ */
+OAKENGINE_API OakEngineMarker *oakengine_marker_create(
+ int color, int64_t in_num, int64_t in_den, int64_t out_num, int64_t out_den,
+ const char *name);
+
+/** @brief Free a detached marker created by oakengine_marker_create(). */
+OAKENGINE_API void oakengine_marker_free(OakEngineMarker *marker);
+
+/** @brief Re-add an existing (removed) marker to the list. Returns
+ * OAKENGINE_OK or OAKENGINE_E_INVALID. */
+OAKENGINE_API int oakengine_marker_list_add_existing(OakEngineMarkerList *list,
+ OakEngineMarker *marker);
+
+/** @brief Marker at the given sorted index, or NULL if out of range. */
+OAKENGINE_API OakEngineMarker *
+oakengine_marker_list_at(const OakEngineMarkerList *list, int index);
+
+/** @brief Find a marker by its exact in-point time (rational seconds).
+ * Returns the marker or NULL if not found. */
+OAKENGINE_API OakEngineMarker *
+oakengine_marker_list_marker_at_time(const OakEngineMarkerList *list,
+ int64_t num, int64_t den);
+
+/** @brief Get the marker's time range as rational seconds. Any pointer
+ * may be NULL. Returns OAKENGINE_OK or OAKENGINE_E_INVALID. */
+OAKENGINE_API int oakengine_marker_get_time(const OakEngineMarker *self,
+ int64_t *in_num, int64_t *in_den,
+ int64_t *out_num,
+ int64_t *out_den);
+
+/** @brief Get the marker's name (buf/size convention). Returns the
+ * would-be length (excluding NUL) or OAKENGINE_E_INVALID. */
+OAKENGINE_API int oakengine_marker_get_name(const OakEngineMarker *self,
+ char *buf, int buf_size);
+
+/** @brief Get the marker's color index. Returns -1 on a NULL handle. */
+OAKENGINE_API int oakengine_marker_get_color(const OakEngineMarker *self);
+
+/** @brief 1 if the marker list has another marker at the given rational
+ * time, 0 otherwise. 0 on a NULL marker handle. */
+OAKENGINE_API int
+oakengine_marker_has_sibling_at_time(const OakEngineMarker *self, int64_t num,
+ int64_t den);
+
+/** @brief Set the marker's time range live (non-undoable). Returns
+ * OAKENGINE_OK or OAKENGINE_E_INVALID. */
+OAKENGINE_API int oakengine_marker_set_time_live(OakEngineMarker *self,
+ int64_t in_num,
+ int64_t in_den,
+ int64_t out_num,
+ int64_t out_den);
+
+/** @brief Commit a time change as an undoable command (undo restores the
+ * pre-commit state). Returns OAKENGINE_OK or OAKENGINE_E_INVALID. */
+OAKENGINE_API int oakengine_marker_commit_time(
+ OakEngineMarker *self, int64_t old_in_num, int64_t old_in_den,
+ int64_t old_out_num, int64_t old_out_den, int64_t new_in_num,
+ int64_t new_in_den, int64_t new_out_num, int64_t new_out_den,
+ void *command);
+
+/**
+ * @brief Create a MarkerChangeTimeCommand as an opaque command pointer.
+ * `new_time_num`/`new_time_den` is the new in-point in rational seconds;
+ * the marker's out-point offset is preserved.
+ */
+OAKENGINE_API void *oakengine_marker_set_time_command(
+ OakEngineMarker *marker, int64_t new_time_num, int64_t new_time_den);
+
+/** @brief Remove the marker from its list (undoable). Returns
+ * OAKENGINE_OK or OAKENGINE_E_INVALID. */
+OAKENGINE_API int oakengine_marker_remove(OakEngineMarker *self);
+
+/** @brief Batch-set properties on one or more markers (undoable, ONE
+ * command). Pass -1 for color to leave it unchanged; pass NULL for name
+ * to leave it unchanged. When `count` == 1, optionally move the marker's
+ * time range. Returns OAKENGINE_OK or OAKENGINE_E_INVALID. */
+OAKENGINE_API int oakengine_marker_set_properties(
+ OakEngineMarker **markers, int count, int color, const char *name,
+ int move_time, int64_t new_in_num, int64_t new_in_den,
+ int64_t new_out_num, int64_t new_out_den, void *command);
+
+/* ---- Workarea handle family ---------------------------------------------------
+ *
+ * Workarea operations on opaque OakEngineWorkarea handles. Create with
+ * oakengine_workarea_create() or borrow from a viewer with
+ * oakengine_viewer_get_workarea_handle(). Standalone workareas must be
+ * freed with oakengine_workarea_free(). All times are rational seconds.
+ */
+
+/** @brief Create a standalone workarea (caller owns it). */
+OAKENGINE_API OakEngineWorkarea *oakengine_workarea_create(void);
+
+/** @brief Free a standalone workarea. NULL-safe. */
+OAKENGINE_API void oakengine_workarea_free(OakEngineWorkarea *wa);
+
+/** @brief Read the workarea state. Any pointer may be NULL. Returns
+ * OAKENGINE_OK or OAKENGINE_E_INVALID. */
+OAKENGINE_API int oakengine_workarea_get(const OakEngineWorkarea *self,
+ int64_t *in_num, int64_t *in_den,
+ int64_t *out_num, int64_t *out_den,
+ int *enabled);
+
+/** @brief Set the workarea range (non-undoable). Returns OAKENGINE_OK or
+ * OAKENGINE_E_INVALID. */
+OAKENGINE_API int oakengine_workarea_set_range(OakEngineWorkarea *self,
+ int64_t in_num, int64_t in_den,
+ int64_t out_num, int64_t out_den);
+
+/** @brief Enable/disable the workarea (non-undoable). Returns
+ * OAKENGINE_OK or OAKENGINE_E_INVALID. */
+OAKENGINE_API int oakengine_workarea_set_enabled(OakEngineWorkarea *self,
+ int enabled);
+
+/** @brief Set the workarea range with undo support. Pass the reset
+ * sentinels (from oakengine_workarea_reset_in_out()) for the old range
+ * when creating fresh. Returns OAKENGINE_OK or OAKENGINE_E_INVALID. */
+OAKENGINE_API int oakengine_workarea_set_range_undoable(
+ OakEngineWorkarea *self, int64_t in_num, int64_t in_den, int64_t out_num,
+ int64_t out_den, int64_t old_in_num, int64_t old_in_den,
+ int64_t old_out_num, int64_t old_out_den, void *command);
+
+/** @brief Enable/disable the workarea with undo support. Pass NULL for
+ * command (creates a standalone undo command that is pushed onto the
+ * global stack when the workarea has an owning project). Returns
+ * OAKENGINE_OK or OAKENGINE_E_INVALID. */
+OAKENGINE_API int oakengine_workarea_set_enabled_undoable(
+ OakEngineWorkarea *self, int enabled, void *command);
+
+/** @brief Fill the reset sentinel values (in = 0/1, out = RATIONAL_MAX).
+ * Any pointer may be NULL. */
+OAKENGINE_API void oakengine_workarea_reset_in_out(int64_t *in_num,
+ int64_t *in_den,
+ int64_t *out_num,
+ int64_t *out_den);
+
+/* ---- Clip media range / cache / media in --------------------------------- */
+
+/** @brief Get the clip's media range as rational seconds
+ * (ClipBlock::media_range()). Any pointer may be NULL. Returns
+ * OAKENGINE_OK or OAKENGINE_E_INVALID. */
+OAKENGINE_API int oakengine_clip_get_media_range_rational(
+ const OakEngineClip *self, int64_t *in_num, int64_t *in_den,
+ int64_t *out_num, int64_t *out_den);
+
+/** @brief Get the clip's media in-point as rational seconds
+ * (ClipBlock::media_in()). Any pointer may be NULL. Returns OAKENGINE_OK
+ * or OAKENGINE_E_INVALID. */
+OAKENGINE_API int oakengine_clip_get_media_in_rational(
+ const OakEngineClip *self, int64_t *num, int64_t *den);
+
+/** @brief Move the clip's media in-point (undoable when undoable != 0,
+ * else direct). Returns OAKENGINE_OK or OAKENGINE_E_INVALID. */
+OAKENGINE_API int oakengine_clip_set_media_in(OakEngineClip *self,
+ int64_t media_in_ts,
+ int undoable);
+
+/** @brief Move the clip's media in-point as a rational seconds value
+ * (undoable when undoable != 0, else direct). This variant does not
+ * require the clip to be on a track yet. Returns OAKENGINE_OK or
+ * OAKENGINE_E_INVALID. */
+OAKENGINE_API int oakengine_clip_set_media_in_rational(OakEngineClip *self,
+ int64_t num,
+ int64_t den,
+ int undoable);
+
+/** @brief Request invalidation of the cache for the given range. NULL-safe
+ * (no-op). */
+OAKENGINE_API void oakengine_clip_request_invalidate(OakEngineClip *self,
+ int64_t in_ts,
+ int64_t out_ts,
+ int type);
+
+/** @brief Add a cache passthrough dependency (copy results from `source`
+ * to `dest`). NULL-safe (no-op). */
+OAKENGINE_API void oakengine_clip_add_cache_passthrough(
+ OakEngineClip *dest, OakEngineClip *source);
+
+/** @brief Discard the clip's cache. NULL-safe (no-op). */
+OAKENGINE_API void oakengine_clip_discard_cache(OakEngineClip *self);
+
+/** @brief Create a new empty ClipBlock. The caller owns the returned node
+ * and must add it to a project (e.g. via oakengine_project_add_node or a
+ * custom undo command) before the engine can manage its lifecycle. The
+ * optional `label` sets the node's user label (Node::set_label()). */
+OAKENGINE_API OakEngineClip *oakengine_clip_create_empty(const char *label);
+
+/** @brief Request invalidated cache ranges from the node connected to the
+ * clip's buffer input (ClipBlock::request_invalidated_from_connected()).
+ * Pass in_den == 0 or out_den == 0 to intersect the full media range. */
+OAKENGINE_API void oakengine_clip_request_invalidate_connected(
+ OakEngineClip *self, int force_all, int64_t in_num, int64_t in_den,
+ int64_t out_num, int64_t out_den);
+
+/* ---- Block functions (generic block, not just ClipBlock) ------------------ */
+
+/** @brief 1 if the block is enabled (Block::is_enabled()). 0 on NULL. */
+OAKENGINE_API int oakengine_block_is_enabled(const OakEngineBlock *self);
+
+/** @brief Enable or disable the block (undoable). Returns OAKENGINE_OK
+ * or OAKENGINE_E_INVALID. */
+OAKENGINE_API int oakengine_block_set_enabled(OakEngineBlock *self,
+ int enabled);
+
+/* ---- Block traversal -------------------------------------------------------- */
+
+/** @brief Number of blocks (including gaps) on the track. Returns
+ * OAKENGINE_E_INVALID for a NULL handle. */
+OAKENGINE_API int oakengine_track_block_count(const OakEngineTrack *track);
+
+/** @brief The block at `index` on the track (0-based, includes gaps).
+ * Returns NULL when out of range or on a NULL handle. */
+OAKENGINE_API OakEngineBlock *
+oakengine_track_block_at(const OakEngineTrack *track, int index);
+
+/** @brief The block at the given timestamp, or NULL if the time falls in
+ * a gap or past the end. Timestamp is in the track's sequence timebase. */
+OAKENGINE_API OakEngineBlock *
+oakengine_track_block_at_time(const OakEngineTrack *track, int64_t timestamp);
+
+/** @brief Nearest block whose out-point is strictly before `timestamp`.
+ * Returns NULL when none. */
+OAKENGINE_API OakEngineBlock *
+oakengine_track_nearest_block_before(const OakEngineTrack *track,
+ int64_t timestamp);
+
+/** @brief Nearest block whose in-point is strictly after `timestamp`.
+ * Returns NULL when none. */
+OAKENGINE_API OakEngineBlock *
+oakengine_track_nearest_block_after(const OakEngineTrack *track,
+ int64_t timestamp);
+
+/** @brief Nearest block whose out-point >= `timestamp`
+ * (i.e. the block containing or immediately before the time).
+ * Returns NULL when none. */
+OAKENGINE_API OakEngineBlock *
+oakengine_track_nearest_block_before_or_at(const OakEngineTrack *track,
+ int64_t timestamp);
+
+/** @brief Nearest block whose in-point <= `timestamp`
+ * (i.e. the block containing or immediately after the time).
+ * Returns NULL when none. */
+OAKENGINE_API OakEngineBlock *
+oakengine_track_nearest_block_after_or_at(const OakEngineTrack *track,
+ int64_t timestamp);
+
+/** @brief 1 if the block is a GapBlock, 0 otherwise. 0 on NULL. */
+OAKENGINE_API int oakengine_block_is_gap(const OakEngineBlock *block);
+
+/** @brief Next block in the track's linked list, or NULL. NULL on NULL. */
+OAKENGINE_API OakEngineBlock *oakengine_block_next(const OakEngineBlock *block);
+
+/** @brief Previous block in the track's linked list, or NULL. NULL on NULL. */
+OAKENGINE_API OakEngineBlock *oakengine_block_prev(const OakEngineBlock *block);
+
+/** @brief Fill `in` and `out` with the block's range as timestamps in the
+ * owning track's sequence timebase. Either pointer may be NULL. Returns
+ * OAKENGINE_OK or OAKENGINE_E_INVALID. */
+OAKENGINE_API int oakengine_block_get_range(const OakEngineBlock *block,
+ int64_t *in, int64_t *out);
+
+/* ---- Clip input ID getters ------------------------------------------------- */
+
+/** @brief ClipBlock::k_buffer_in. Static string, never freed. */
+OAKENGINE_API const char *oakengine_clip_buffer_input_id(void);
+/** @brief ClipBlock::k_speed_input. */
+OAKENGINE_API const char *oakengine_clip_speed_input_id(void);
+/** @brief ClipBlock::k_reverse_input. */
+OAKENGINE_API const char *oakengine_clip_reverse_input_id(void);
+/** @brief ClipBlock::k_maintain_audio_pitch_input. */
+OAKENGINE_API const char *
+oakengine_clip_maintain_audio_pitch_input_id(void);
+/** @brief ClipBlock::k_loop_mode_input. */
+OAKENGINE_API const char *oakengine_clip_loop_mode_input_id(void);
+/** @brief ClipBlock::k_auto_cache_input. */
+OAKENGINE_API const char *oakengine_clip_auto_cache_input_id(void);
+
+/* ---- Sequence: add_default_nodes ------------------------------------------ */
+
+/** @brief Add one video and one audio track as ONE undoable command
+ * (ViewerOutput helper used by the application). Returns OAKENGINE_OK
+ * or OAKENGINE_E_INVALID. */
+OAKENGINE_API int
+oakengine_sequence_add_default_nodes(OakEngineSequence *seq);
+
+/* ---- Sequence: add_sequence_clip ------------------------------------------- */
+
+/** @brief Place a nested Sequence as a clip on a track (undoable).
+ *
+ * Same semantics as oakengine_sequence_add_footage_clip() but creates a
+ * clip whose buffer input feeds from another Sequence node (nested
+ * timeline). Self-nesting and circular nesting are detected and rejected.
+ * Returns a borrowed clip handle or NULL on failure. */
+OAKENGINE_API OakEngineClip *
+oakengine_sequence_add_sequence_clip(OakEngineSequence *seq,
+ OakEngineSequence *nested,
+ int track_type, int track_index,
+ int64_t in, int64_t out,
+ int64_t media_in);
+
+/* ---- Track handle queries -------------------------------------------------- */
+
+/** @brief Borrowed track handle, or NULL if the track does not exist. */
+OAKENGINE_API OakEngineTrack *
+oakengine_sequence_track_at(const OakEngineSequence *seq, int track_type,
+ int track_index);
+
+/** @brief Track type (OAKENGINE_TRACK_TYPE_*), or -1 on a NULL handle. */
+OAKENGINE_API int oakengine_track_type(const OakEngineTrack *track);
+
+/** @brief Track content length in frame timestamps (Track::get_length()
+ * converted to timebase units). Returns OAKENGINE_OK or
+ * OAKENGINE_E_NOT_FOUND/OAKENGINE_E_INVALID. */
+OAKENGINE_API int oakengine_track_get_length(const OakEngineSequence *seq,
+ int track_type, int track_index,
+ int64_t *length);
+
+/** @brief 1 if the range [in_ts, out_ts) is free (no blocks intersect it),
+ * 0 if it intersects. Returns OAKENGINE_E_NOT_FOUND when the track does
+ * not exist, OAKENGINE_E_INVALID for bad arguments. */
+OAKENGINE_API int oakengine_track_is_range_free(const OakEngineSequence *seq,
+ int track_type,
+ int track_index,
+ int64_t in_ts, int64_t out_ts);
+
+/** @brief Track height helpers (matching the engine's
+ * Track::k_height_* constants). */
+OAKENGINE_API double oakengine_track_height_default(void);
+OAKENGINE_API int oakengine_track_default_height_in_pixels(void);
+OAKENGINE_API int oakengine_track_height_internal_to_pixels(double height);
+OAKENGINE_API double oakengine_track_height_pixels_to_internal(int pixels);
+
+/** @brief Track height step interval (e.g. 0.5). > 0.0. */
+OAKENGINE_API double oakengine_track_height_interval(void);
+
+/** @brief Minimum track height (e.g. 1.5). > 0.0. */
+OAKENGINE_API double oakengine_track_height_minimum(void);
+
+/* ---- Multicam helpers --------------------------------------------------- */
+
+/** @brief Find the MultiCamNode ancestor of a clip, or NULL. Accepts
+ * OakEngineNode* (a clip or any node). */
+OAKENGINE_API OakEngineNode *
+oakengine_clip_find_multicam(OakEngineNode *node);
+
+/** @brief Switch the multicam source to the given track/stream at the
+ * given time. Returns OAKENGINE_OK or OAKENGINE_E_INVALID. */
+OAKENGINE_API int oakengine_multicam_switch_source(
+ OakEngineNode *multicam_node, OakEngineNode *footage_node,
+ int track_type, int track_index, double time_seconds,
+ void *command);
+
#ifdef __cplusplus
}
#endif
diff --git a/engine/include/oakengine/traverse.h b/engine/include/oakengine/traverse.h
new file mode 100644
index 000000000..b920f127f
--- /dev/null
+++ b/engine/include/oakengine/traverse.h
@@ -0,0 +1,167 @@
+/***
+
+ 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 .
+
+***/
+
+#ifndef OAKENGINE_TRAVERSE_H
+#define OAKENGINE_TRAVERSE_H
+
+#include
+
+#include "export.h"
+#include "init.h"
+#include "node.h"
+#include "videoparams.h"
+
+#ifdef __cplusplus
+extern "C" {
+#endif
+
+/**
+ * @file traverse.h
+ * @brief C ABI for synchronous node-graph value evaluation
+ * (olive::NodeTraverser)
+ *
+ * The engine evaluates a node's value at a given time by traversing its
+ * input graph (olive::NodeTraverser). The application uses this in three
+ * places: the node table view (per-input value databases), the node value
+ * tree (one output table + the element a downstream input's value hint
+ * selects) and the viewer display gizmos (a transform between two nodes
+ * and the gizmo node's input row at drag start). This family exposes
+ * those paths without leaking NodeValueTable/NodeValueRow C++ types.
+ *
+ * All evaluation is SYNCHRONOUS on the calling thread and CPU-only in
+ * this family (textures are resolved as engine-side dummy textures, which
+ * is exactly what the table/tree views need -- they only read metadata).
+ * Call from the GUI thread.
+ *
+ * OakEngineTraverseDb is an OWNED result object; free it with
+ * oakengine_traverse_db_free(). Strings it returns point into the object
+ * and are valid until freed. Times are Rational seconds as int64
+ * numerator/denominator pairs, like the rest of the facade.
+ */
+
+typedef struct OakEngineTraverseDb OakEngineTraverseDb;
+
+/**
+ * @brief Evaluate every input of `node` over [in_num/in_den,
+ * out_num/out_den] seconds and return the per-input value database
+ * (NodeTraverser::generate_database()). NULL on invalid arguments.
+ */
+OAKENGINE_API OakEngineTraverseDb *oakengine_traverse_generate_database(
+ OakEngineNode *node, int64_t in_num, int64_t in_den, int64_t out_num,
+ int64_t out_den);
+
+/**
+ * @brief Evaluate the single output table of `node`
+ * (NodeTraverser::generate_table()). Returned as a database with exactly
+ * one entry whose input id is an empty string.
+ */
+OAKENGINE_API OakEngineTraverseDb *oakengine_traverse_generate_table(
+ OakEngineNode *node, int64_t in_num, int64_t in_den, int64_t out_num,
+ int64_t out_den);
+
+/** @brief Free a database returned by this family. NULL is a no-op. */
+OAKENGINE_API void oakengine_traverse_db_free(OakEngineTraverseDb *db);
+
+/** @brief Number of input entries (generate_database: one per input id
+ * that produced a table; generate_table: exactly 1). */
+OAKENGINE_API int oakengine_traverse_db_input_count(
+ const OakEngineTraverseDb *db);
+
+/** @brief Input id of entry `input_index` (valid until db is freed). */
+OAKENGINE_API const char *oakengine_traverse_db_input_id(
+ const OakEngineTraverseDb *db, int input_index);
+
+/** @brief Row count of the table at `input_index`
+ * (NodeValueTable::count()). */
+OAKENGINE_API int oakengine_traverse_db_row_count(
+ const OakEngineTraverseDb *db, int input_index);
+
+/**
+ * @brief Row accessors. `row` is 0-based in table order (the views
+ * reverse it themselves where needed). Strings are valid until db is
+ * freed.
+ *
+ * - type: oak_node_value_type of the value.
+ * - source: borrowed node that produced the value, or NULL.
+ * - tag: the value's tag (may be empty, never NULL).
+ * - value_string: NodeValue::value_to_string(value, false).
+ * - split_count / split_string: NodeValue::to_split_value() count and
+ * NodeValue::value_to_string(type, split[k], true) per element.
+ */
+OAKENGINE_API int oakengine_traverse_row_type(const OakEngineTraverseDb *db,
+ int input_index, int row);
+OAKENGINE_API OakEngineNode *oakengine_traverse_row_source(
+ const OakEngineTraverseDb *db, int input_index, int row);
+OAKENGINE_API const char *oakengine_traverse_row_tag(
+ const OakEngineTraverseDb *db, int input_index, int row);
+OAKENGINE_API const char *oakengine_traverse_row_value_string(
+ const OakEngineTraverseDb *db, int input_index, int row);
+OAKENGINE_API int oakengine_traverse_row_split_count(
+ const OakEngineTraverseDb *db, int input_index, int row);
+OAKENGINE_API const char *oakengine_traverse_row_split_string(
+ const OakEngineTraverseDb *db, int input_index, int row, int split);
+
+/**
+ * @brief The table element selected by `hint_node`'s value hint for input
+ * `input_id`@`element` against a table produced by
+ * oakengine_traverse_generate_table() (pass its db; must contain exactly
+ * one entry) -- NodeTraverser::generate_row_value_element_index().
+ * Returns -1 when no element matches.
+ */
+OAKENGINE_API int oakengine_traverse_table_element_index_for_hint(
+ OakEngineNode *hint_node, const char *input_id, int element,
+ const OakEngineTraverseDb *table_db);
+
+/**
+ * @brief Fill a caller-allocated olive::NodeValueRow with `node`'s input
+ * values over the given range (NodeTraverser::generate_row()) -- the
+ * viewer display gizmo drag-start path. `cache_video_params` (may be NULL
+ * for engine defaults) and `sample_rate`/`channel_layout` seed the
+ * traverser's cache params (NodeTraverser::set_cache_video_params /
+ * set_cache_audio_params).
+ *
+ * Transition bridge (same pattern as
+ * replaced by internal ColorTransformJob API): `row_out` is opaque to C
+ * consumers; the application passes a pointer to its own
+ * olive::NodeValueRow (a QHash typedef, no engine symbols) which the
+ * engine fills in place.
+ */
+OAKENGINE_API int oakengine_traverse_generate_row(
+ OakEngineNode *node, int64_t in_num, int64_t in_den, int64_t out_num,
+ int64_t out_den, const oak_video_params *cache_video_params,
+ int sample_rate, uint64_t channel_layout, void *row_out);
+
+/**
+ * @brief Accumulate the transform from `start` to `end` over the given
+ * range (NodeTraverser::transform()) and return it as the 6 affine
+ * coefficients of a QTransform (m11, m12, m21, m22, dx, dy), suitable for
+ * `QTransform(m11, m12, m21, m22, dx, dy)`. `cache_video_params` may be
+ * NULL for engine defaults.
+ */
+OAKENGINE_API int oakengine_traverse_transform(
+ OakEngineNode *start, OakEngineNode *end, int64_t in_num, int64_t in_den,
+ int64_t out_num, int64_t out_den, const oak_video_params *cache_video_params,
+ double out_m[6]);
+
+#ifdef __cplusplus
+}
+#endif
+
+#endif /* OAKENGINE_TRAVERSE_H */
diff --git a/engine/include/oakengine/undo.h b/engine/include/oakengine/undo.h
new file mode 100644
index 000000000..0c3bc6b28
--- /dev/null
+++ b/engine/include/oakengine/undo.h
@@ -0,0 +1,272 @@
+/***
+
+ 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 .
+
+***/
+
+#ifndef OAKENGINE_UNDO_H
+#define OAKENGINE_UNDO_H
+
+#include
+
+#include "init.h"
+
+#ifdef __cplusplus
+extern "C" {
+#endif
+
+/**
+ * @file undo.h
+ * @brief C ABI for the global undo stack (olive::UndoStack)
+ *
+ * Exposes the process-wide undo stack that backs every editing primitive:
+ * pushing commands (the command objects themselves are still created by
+ * the caller as opaque engine pointers), jumping to an arbitrary history
+ * position, reading the command list for a history view, and the
+ * undo/redo QActions for menus.
+ *
+ * Change notification: subscribe to OAKENGINE_EVENT_UNDO_INDEX_CHANGED on
+ * oakengine_undo_handle(). The event fires after every stack mutation
+ * (push/undo/redo/jump/clear); the command list must be re-read through
+ * oakengine_undo_count()/oakengine_undo_command_text().
+ *
+ * Conventions (matching oakengine/project.h):
+ * - Return codes: 0 (OAKENGINE_OK) on success, negative OAKENGINE_E_*
+ * on failure. Functions returning a value return OAKENGINE_E_INVALID
+ * when no application core exists.
+ * - String output uses the buf/size convention.
+ */
+
+/**
+ * @brief Borrowed handle of the global undo stack, for use as the
+ * subscription handle of OAKENGINE_EVENT_UNDO_INDEX_CHANGED. Returns NULL
+ * when no application core exists.
+ */
+OAKENGINE_API void *oakengine_undo_handle(void);
+
+/**
+ * @brief Push `command` (an opaque `olive::UndoCommand *`, e.g. the result
+ * of oakengine_task_import_get_command()) onto the stack and execute its
+ * redo. Takes ownership of `command` (an empty MultiUndoCommand is deleted
+ * immediately, matching UndoStack::push). `name` is the user-visible
+ * command label (NULL behaves like an empty label).
+ */
+OAKENGINE_API int oakengine_undo_push(void *command, const char *name);
+
+/**
+ * @brief Start collecting: subsequent facade undoable operations are added
+ * as children to a group and executed eagerly, but not pushed individually.
+ * Returns OAKENGINE_E_STATE if a group is already open.
+ */
+OAKENGINE_API int oakengine_undo_group_begin(const char *name);
+
+/**
+ * @brief End the group and push it as ONE undo entry.
+ *
+ * An empty group is discarded (no undo entry). Returns OAKENGINE_E_STATE
+ * if no group is open.
+ */
+OAKENGINE_API int oakengine_undo_group_end(void);
+
+/**
+ * @brief Abort the open group: undo all already-executed children and
+ * discard the group. Returns OAKENGINE_E_STATE if no group is open.
+ */
+OAKENGINE_API int oakengine_undo_group_abort(void);
+
+/**
+ * @brief Execute the redo of `command` without taking ownership
+ * (UndoCommand::redo_now semantics). This is the facade replacement for
+ * app code that used to call MultiUndoCommand::redo_now() directly.
+ */
+OAKENGINE_API int oakengine_undo_command_redo_now(void *command);
+
+/**
+ * @brief Execute the undo of `command` without taking ownership
+ * (UndoCommand::undo_now semantics).
+ */
+OAKENGINE_API int oakengine_undo_command_undo_now(void *command);
+
+/**
+ * @brief Callback signatures for app-defined undo commands.
+ *
+ * These allow UI-side code to create undoable actions without defining
+ * C++ subclasses of olive::UndoCommand. The engine wraps the callbacks
+ * in an internal UndoCommand and forwards redo/undo/free calls.
+ */
+typedef void (*oakengine_undo_command_redo_fn)(void *userdata);
+typedef void (*oakengine_undo_command_undo_fn)(void *userdata);
+typedef void (*oakengine_undo_command_free_fn)(void *userdata);
+
+/**
+ * @brief Create an app-defined undo command backed by C callbacks.
+ *
+ * The returned pointer is an opaque `olive::UndoCommand *` suitable for
+ * oakengine_undo_push() or oakengine_undo_command_multi_add_child().
+ * The command takes ownership of `userdata`; `free_fn` is called when
+ * the command is destroyed (whether pushed or freed directly).
+ *
+ * `name` is the user-visible label. Any callback may be NULL; a NULL
+ * redo/undo callback makes that direction a no-op.
+ */
+OAKENGINE_API void *oakengine_undo_command_create(
+ const char *name,
+ oakengine_undo_command_redo_fn redo,
+ oakengine_undo_command_undo_fn undo,
+ oakengine_undo_command_free_fn free_fn,
+ void *userdata);
+
+/**
+ * @brief Create an empty MultiUndoCommand as an opaque command pointer.
+ *
+ * The returned pointer is owned by the caller until it is passed to
+ * oakengine_undo_push() or freed with oakengine_undo_command_free().
+ */
+OAKENGINE_API void *oakengine_undo_command_create_multi(void);
+
+OAKENGINE_API void *oakengine_node_add_command(void *project, void *node);
+OAKENGINE_API void *oakengine_node_set_position_command(
+ void *node, void *context, double x, double y, int expanded);
+OAKENGINE_API void *oakengine_node_remove_position_command(
+ void *node, void *context);
+OAKENGINE_API void *oakengine_node_set_value_hint_command(
+ void *node, const char *input, int element, int type, int index,
+ const char *tag);
+OAKENGINE_API void *oakengine_node_remove_and_disconnect_command(void *node);
+
+OAKENGINE_API void *oakengine_track_place_block_command(
+ void *track_list, int track_index, void *block, int64_t in_ts);
+OAKENGINE_API void *oakengine_track_replace_block_with_gap_command(
+ void *track, void *block, int handle_transitions);
+OAKENGINE_API void *oakengine_block_trim_command(
+ void *track, void *block, int64_t new_length_num, int64_t new_length_den,
+ int movement_mode, int roll_edit);
+OAKENGINE_API void *oakengine_transition_remove_command(
+ void *transition, int remove_from_graph);
+OAKENGINE_API void *oakengine_track_slide_command(
+ void *track, void *const *blocks, int block_count,
+ void *in_adjacent, void *out_adjacent,
+ int64_t movement_num, int64_t movement_den);
+OAKENGINE_API void *oakengine_block_split_preserving_links_command(
+ void *const *blocks, int count, int64_t point_ts);
+OAKENGINE_API void *oakengine_block_split_get_split(
+ void *command, void *block, int time_index);
+OAKENGINE_API void *oakengine_block_resize_with_media_in_command(
+ void *block, int64_t length_num, int64_t length_den);
+OAKENGINE_API void *oakengine_block_set_media_in_command(
+ void *block, int64_t media_in_num, int64_t media_in_den);
+OAKENGINE_API void *oakengine_timeline_ripple_delete_gaps_command(
+ void *sequence, const int64_t *range_in_ts, const int64_t *range_out_ts,
+ const int *track_types, const int *track_indexes, int range_count);
+
+/**
+ * @brief Create a TrackListInsertGaps command as an opaque command pointer.
+ * `point_num`/`point_den` is the insertion point in rational seconds;
+ * `length_num`/`length_den` is the gap length in rational seconds.
+ */
+OAKENGINE_API void *oakengine_track_list_insert_gaps_command(
+ void *track_list, int64_t point_num, int64_t point_den,
+ int64_t length_num, int64_t length_den);
+
+/**
+ * @brief Add `child` (an opaque command pointer) to the MultiUndoCommand
+ * `multi`. Returns OAKENGINE_OK on success, OAKENGINE_E_INVALID if either
+ * argument is NULL.
+ */
+OAKENGINE_API int oakengine_undo_command_multi_add_child(void *multi,
+ void *child);
+
+/**
+ * @brief Return the number of children in the MultiUndoCommand `multi`,
+ * or OAKENGINE_E_INVALID if `multi` is NULL.
+ */
+OAKENGINE_API int oakengine_undo_command_multi_child_count(void *multi);
+
+/**
+ * @brief Destroy a command created by oakengine_undo_command_create() or
+ * oakengine_undo_command_create_multi() without pushing it onto the stack.
+ * Commands passed to oakengine_undo_push() are owned by the stack and
+ * must not be freed by the caller.
+ */
+OAKENGINE_API void oakengine_undo_command_free(void *command);
+
+/**
+ * @brief Total number of history rows (done + undone commands), or
+ * OAKENGINE_E_INVALID when no stack exists.
+ */
+OAKENGINE_API int64_t oakengine_undo_count(void);
+
+/**
+ * @brief Current position in the history: the number of done commands
+ * (rows below this index are undone). Emitted as payload `a` of
+ * OAKENGINE_EVENT_UNDO_INDEX_CHANGED.
+ */
+OAKENGINE_API int64_t oakengine_undo_index(void);
+
+/**
+ * @brief Label of the history row at `row` (0-based, buf/size convention).
+ * Falls back to the translated "Command" placeholder for empty labels.
+ *
+ * @return the label length, or OAKENGINE_E_NOT_FOUND for an invalid row.
+ */
+OAKENGINE_API int oakengine_undo_command_text(int64_t row, char *buf,
+ int buf_size);
+
+/**
+ * @brief 1 when the row at `row` is currently done (not undone), 0 when it
+ * is undone, OAKENGINE_E_NOT_FOUND for an invalid row.
+ */
+OAKENGINE_API int oakengine_undo_command_is_done(int64_t row);
+
+/**
+ * @brief Undo/redo until the done-command count equals `index`
+ * (UndoStack::jump semantics).
+ */
+OAKENGINE_API int oakengine_undo_jump(int64_t index);
+
+/**
+ * @brief Delete all commands and push the fresh "New/Open Project" empty
+ * command (UndoStack::clear).
+ */
+OAKENGINE_API int oakengine_undo_clear(void);
+
+/**
+ * @brief Refresh the undo/redo action labels and enabled state
+ * (UndoStack::update_actions).
+ */
+OAKENGINE_API int oakengine_undo_update_actions(void);
+
+/**
+ * @brief 1/0 whether undo (redo) is currently possible,
+ * OAKENGINE_E_INVALID when no stack exists.
+ */
+OAKENGINE_API int oakengine_undo_can_undo(void);
+OAKENGINE_API int oakengine_undo_can_redo(void);
+
+/**
+ * @brief The stack's undo (redo) QAction as an opaque `void *` (actually a
+ * `QAction *`; Qt types are allowed at this boundary). Borrowed; owned by
+ * the stack. NULL when no stack exists.
+ */
+OAKENGINE_API void *oakengine_undo_undo_action(void);
+OAKENGINE_API void *oakengine_undo_redo_action(void);
+
+#ifdef __cplusplus
+}
+#endif
+
+#endif /* OAKENGINE_UNDO_H */
diff --git a/engine/include/oakengine/videoparams.h b/engine/include/oakengine/videoparams.h
new file mode 100644
index 000000000..bcc87acfb
--- /dev/null
+++ b/engine/include/oakengine/videoparams.h
@@ -0,0 +1,211 @@
+/***
+
+ 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 .
+
+***/
+
+#ifndef OAKENGINE_VIDEOPARAMS_H
+#define OAKENGINE_VIDEOPARAMS_H
+
+#include
+
+#include "export.h"
+
+#ifdef __cplusplus
+extern "C" {
+#endif
+
+/**
+ * @file videoparams.h
+ * @brief C ABI POD and static-data accessors for the engine's VideoParams
+ *
+ * Covers the parameter surface the export and sequence dialogs need:
+ * the POD carried by the encoding family (oakengine/encoding.h) and the
+ * static metadata behind the standard combo boxes (supported frame rates,
+ * pixel aspect ratios, dividers, pixel format names). Display/render-path
+ * helpers (bytes per pixel, scaled texture sizes, ...) are out of scope for
+ * now.
+ *
+ * Conventions match the other facade families: buf/size strings (return
+ * value is the would-be length excluding the NUL), -1 on invalid indexes,
+ * 0 (OAKENGINE_OK) / negative OAKENGINE_E_* codes where applicable.
+ */
+
+/**
+ * @brief POD mirror of olive::VideoParams' user-facing fields.
+ *
+ * `time_base_*` is the frame duration (frame rate flipped), matching
+ * VideoParams::time_base(); the frame rate is den/num. `format` is an
+ * olive::PixelFormat::Format value, `interlacing` an
+ * olive::VideoParams::Interlacing value (0 = none/progressive, 1 = top
+ * field first, 2 = bottom field first), `color_range` an
+ * olive::VideoParams::ColorRange value. The video channel count is an
+ * engine-internal constant and not exposed.
+ */
+typedef struct oak_video_params {
+ int width;
+ int height;
+ int time_base_num; /**< Frame duration numerator (e.g. 1001/30000 s). */
+ int time_base_den;
+ int format; /**< olive::PixelFormat::Format. */
+ int pixel_aspect_num;
+ int pixel_aspect_den;
+ int interlacing; /**< olive::VideoParams::Interlacing. */
+ int color_range; /**< olive::VideoParams::ColorRange. */
+ int divider; /**< Preview resolution divider (1 = full). */
+ /* The two fields below are only populated by the viewer family
+ * (oakengine_viewer_get_video_params(), B8c); other producers leave
+ * them 0 (k_video_type_video / not premultiplied). */
+ int video_type; /**< olive::VideoParams::Type. */
+ int premultiplied_alpha; /**< 0/1. */
+} oak_video_params;
+
+/** @brief Number of standard frame rates (VideoParams::k_supported_frame_rates). */
+OAKENGINE_API int oakengine_video_params_supported_frame_rate_count(void);
+
+/**
+ * @brief The `index`-th standard frame rate as num/den (e.g. 24000/1001);
+ * OAKENGINE_E_INVALID when out of range.
+ */
+OAKENGINE_API int oakengine_video_params_supported_frame_rate_at(int index,
+ int *num,
+ int *den);
+
+/**
+ * @brief User-friendly label of a frame rate num/den pair
+ * (VideoParams::frame_rate_to_string(); buf/size).
+ */
+OAKENGINE_API int oakengine_video_params_frame_rate_to_string(int num, int den,
+ char *buf,
+ int buf_size);
+
+/** @brief Number of standard pixel aspect ratios. */
+OAKENGINE_API int oakengine_video_params_standard_pixel_aspect_count(void);
+
+/** @brief The `index`-th standard pixel aspect ratio as num/den. */
+OAKENGINE_API int oakengine_video_params_standard_pixel_aspect_at(int index,
+ int *num,
+ int *den);
+
+/** @brief Display name of the `index`-th standard pixel aspect (buf/size). */
+OAKENGINE_API int
+oakengine_video_params_standard_pixel_aspect_name(int index, char *buf,
+ int buf_size);
+
+/**
+ * @brief VideoParams::format_pixel_aspect_ratio_string(): formats `format`
+ * (a printf-style "%1" template) with the pixel aspect ratio num/den
+ * (buf/size).
+ */
+OAKENGINE_API int oakengine_video_params_format_pixel_aspect_ratio_string(
+ const char *format, int num, int den, char *buf, int buf_size);
+
+/** @brief Number of supported preview dividers. */
+OAKENGINE_API int oakengine_video_params_supported_divider_count(void);
+
+/** @brief The `index`-th supported divider; -1 when out of range. */
+OAKENGINE_API int oakengine_video_params_supported_divider_at(int index);
+
+/** @brief Display name of a divider (VideoParams::get_name_for_divider()). */
+OAKENGINE_API int oakengine_video_params_divider_name(int divider, char *buf,
+ int buf_size);
+
+/**
+ * @brief 1 when `format` (a PixelFormat::Format value) is a float format
+ * (VideoParams::format_is_float()).
+ */
+OAKENGINE_API int oakengine_video_params_format_is_float(int format);
+
+/** @brief Display name of a PixelFormat::Format value (buf/size). */
+OAKENGINE_API int oakengine_video_params_pixel_format_name(int format,
+ char *buf,
+ int buf_size);
+
+/**
+ * @brief Effective (divider-scaled) dimensions of width/height at `divider`
+ * (VideoParams::effective_width()/effective_height()). Any output pointer
+ * may be NULL.
+ *
+ * @return OAKENGINE_OK, or OAKENGINE_E_INVALID for non-positive
+ * width/height/divider.
+ */
+OAKENGINE_API int oakengine_video_params_effective_size(int width, int height,
+ int divider,
+ int *out_width,
+ int *out_height);
+
+/**
+ * @brief Fill an oak_video_params POD (the display-path VideoParams
+ * constructor equivalent). No validation is performed beyond rejecting a
+ * NULL `p`; use oakengine_video_params_is_valid() to validate.
+ *
+ * @return OAKENGINE_OK, or OAKENGINE_E_INVALID for NULL `p`.
+ */
+OAKENGINE_API int oakengine_video_params_make(oak_video_params *p, int width,
+ int height, int time_base_num,
+ int time_base_den, int format,
+ int pixel_aspect_num,
+ int pixel_aspect_den,
+ int interlacing, int color_range,
+ int divider);
+
+/**
+ * @brief Create an engine-side olive::VideoParams object from a POD.
+ *
+ * The returned pointer must be freed with oakengine_video_params_free().
+ * This is the only legal way for app code to construct a VideoParams object
+ * during the R6 C ABI migration.
+ *
+ * @return Engine-owned VideoParams pointer, or NULL if pod is NULL.
+ */
+OAKENGINE_API void *oakengine_video_params_create(const oak_video_params *pod);
+
+/** @brief Free a VideoParams object created by oakengine_video_params_create(). */
+OAKENGINE_API void oakengine_video_params_free(void *params);
+
+/**
+ * @brief 1 when all user-facing fields of `a` and `b` match
+ * (VideoParams::operator==), 0 otherwise or when either is NULL.
+ */
+OAKENGINE_API int oakengine_video_params_equal(const oak_video_params *a,
+ const oak_video_params *b);
+
+/**
+ * @brief 1 when the POD describes a usable video stream
+ * (VideoParams::is_valid(): positive dimensions, non-null pixel aspect,
+ * in-range pixel format), 0 otherwise or when `p` is NULL.
+ */
+OAKENGINE_API int oakengine_video_params_is_valid(const oak_video_params *p);
+
+/**
+ * @brief Bytes per pixel of `format` (a PixelFormat::Format value) with
+ * `channels` channels (VideoParams::get_bytes_per_pixel()).
+ */
+OAKENGINE_API int oakengine_video_params_bytes_per_pixel(int format,
+ int channels);
+
+/**
+ * @brief The engine-internal video channel count
+ * (VideoParams::k_internal_channel_count, i.e. RGBA).
+ */
+OAKENGINE_API int oakengine_video_params_internal_channel_count(void);
+
+#ifdef __cplusplus
+}
+#endif
+
+#endif /* OAKENGINE_VIDEOPARAMS_H */
diff --git a/engine/include/oakengine/viewer.h b/engine/include/oakengine/viewer.h
new file mode 100644
index 000000000..183f64e99
--- /dev/null
+++ b/engine/include/oakengine/viewer.h
@@ -0,0 +1,350 @@
+/***
+
+ 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 .
+
+***/
+
+#ifndef OAKENGINE_VIEWER_H
+#define OAKENGINE_VIEWER_H
+
+#include
+
+#include "export.h"
+#include "init.h"
+#include "node.h"
+#include "timeline.h"
+#include "videoparams.h"
+
+#ifdef __cplusplus
+extern "C" {
+#endif
+
+/**
+ * @file viewer.h
+ * @brief C ABI for viewer nodes (olive::ViewerOutput and subclasses:
+ * Sequence, Footage)
+ *
+ * A viewer node is the bridge between a node graph and a monitor: it owns
+ * a playhead, a length, per-stream video/audio/subtitle parameters, a
+ * workarea and a marker list. This family covers the application-side
+ * uses of olive::ViewerOutput that are not already exposed through the
+ * sequence (timeline.h) or node (node.h) families.
+ *
+ * Handles: a viewer handle is simply an OakEngineNode* whose engine object
+ * is a ViewerOutput (validate with oakengine_viewer_from_node()). Borrowed,
+ * same lifetime rules as node.h. Change notifications (length/playhead/
+ * params/workarea-adjacent) are delivered through the event mechanism --
+ * subscribe with the OAKENGINE_EVENT_VIEWER_* ids from oakengine/events.h
+ * on the node handle.
+ *
+ * Conventions match the rest of the facade: rationals are int64
+ * numerator/denominator pairs (seconds), booleans are int, 0
+ * (OAKENGINE_OK)/negative OAKENGINE_E_* return codes, NULL handles are
+ * no-ops returning OAKENGINE_E_INVALID.
+ */
+
+/**
+ * @brief POD snapshot of a viewer's workarea (olive::TimelineWorkArea:
+ * range in/out + enabled flag). Rationals in seconds.
+ */
+typedef struct oakengine_viewer_workarea {
+ int64_t in_num;
+ int64_t in_den;
+ int64_t out_num;
+ int64_t out_den;
+ int enabled;
+} oakengine_viewer_workarea;
+
+/**
+ * @brief Return `node` if its engine object is a viewer (olive::ViewerOutput
+ * or subclass, e.g. Sequence/Footage), NULL otherwise. Replaces
+ * dynamic_cast at the app boundary; also the canonical way
+ * to validate a handle for this family.
+ */
+OAKENGINE_API OakEngineNode *oakengine_viewer_from_node(OakEngineNode *node);
+
+/** @brief const overload of oakengine_viewer_from_node(). */
+OAKENGINE_API const OakEngineNode *
+oakengine_viewer_from_const_node(const OakEngineNode *node);
+
+/* ---- Input ids / constants (ViewerOutput::k_* statics) ------------------ */
+
+/** @brief ViewerOutput::k_video_params_input. Static string, never freed. */
+OAKENGINE_API const char *oakengine_viewer_video_params_input_id(void);
+/** @brief ViewerOutput::k_audio_params_input. */
+OAKENGINE_API const char *oakengine_viewer_audio_params_input_id(void);
+/** @brief ViewerOutput::k_subtitle_params_input. */
+OAKENGINE_API const char *oakengine_viewer_subtitle_params_input_id(void);
+/** @brief ViewerOutput::k_texture_input. */
+OAKENGINE_API const char *oakengine_viewer_texture_input_id(void);
+/** @brief ViewerOutput::k_samples_input. */
+OAKENGINE_API const char *oakengine_viewer_samples_input_id(void);
+/** @brief ViewerOutput::k_default_sample_format (olive::core::SampleFormat). */
+OAKENGINE_API int oakengine_viewer_default_sample_format(void);
+
+/* ---- Playhead / length --------------------------------------------------- */
+
+/** @brief Current playhead in seconds (ViewerOutput::get_playhead()). */
+OAKENGINE_API int oakengine_viewer_get_playhead(const OakEngineNode *self,
+ int64_t *num, int64_t *den);
+
+/** @brief Move the playhead (ViewerOutput::set_playhead()). Emits
+ * OAKENGINE_EVENT_VIEWER_PLAYHEAD_CHANGED. */
+OAKENGINE_API int oakengine_viewer_set_playhead(OakEngineNode *self,
+ int64_t num, int64_t den);
+
+/**
+ * @brief Set the video parameters of stream `index` on `self`
+ * (ViewerOutput::set_video_params()). `self` must be a viewer node.
+ */
+OAKENGINE_API int oakengine_viewer_set_video_params(OakEngineNode *self,
+ const oak_video_params *params,
+ int index);
+
+/**
+ * @brief Set the audio parameters of stream `index` on `self`
+ * (ViewerOutput::set_audio_params()). `self` must be a viewer node.
+ */
+OAKENGINE_API int oakengine_viewer_set_audio_params(OakEngineNode *self,
+ int sample_rate,
+ uint64_t channel_layout,
+ int format, int index);
+
+/** @brief Content length in seconds (ViewerOutput::get_length()). */
+OAKENGINE_API int oakengine_viewer_get_length(const OakEngineNode *self,
+ int64_t *num, int64_t *den);
+
+/** @brief Video content length in seconds (ViewerOutput::get_video_length()). */
+OAKENGINE_API int oakengine_viewer_get_video_length(const OakEngineNode *self,
+ int64_t *num,
+ int64_t *den);
+
+/** @brief Audio content length in seconds (ViewerOutput::get_audio_length()). */
+OAKENGINE_API int oakengine_viewer_get_audio_length(const OakEngineNode *self,
+ int64_t *num,
+ int64_t *den);
+
+/* ---- Stream parameters ---------------------------------------------------- */
+
+/**
+ * @brief Video params of stream `index` (ViewerOutput::get_video_params()).
+ * `out` is always written; an out-of-range index yields a zeroed struct
+ * (width/height 0 = invalid, matches an invalid olive::VideoParams).
+ */
+OAKENGINE_API int oakengine_viewer_get_video_params(
+ const OakEngineNode *self, int index, oak_video_params *out);
+
+/**
+ * @brief Audio params of stream `index` (ViewerOutput::get_audio_params()).
+ * Any of the out pointers may be NULL. `format` is an
+ * olive::core::SampleFormat value; out-of-range index yields 0/0/0.
+ */
+OAKENGINE_API int oakengine_viewer_get_audio_params(
+ const OakEngineNode *self, int index, int *sample_rate,
+ uint64_t *channel_layout, int *format);
+
+/** @brief Number of video streams (ViewerOutput::get_video_stream_count()). */
+OAKENGINE_API int oakengine_viewer_get_video_stream_count(
+ const OakEngineNode *self);
+/** @brief Number of audio streams (ViewerOutput::get_audio_stream_count()). */
+OAKENGINE_API int oakengine_viewer_get_audio_stream_count(
+ const OakEngineNode *self);
+/** @brief Number of subtitle streams (ViewerOutput::get_subtitle_stream_count()). */
+OAKENGINE_API int oakengine_viewer_get_subtitle_stream_count(
+ const OakEngineNode *self);
+
+/**
+ * @brief 1 if stream `index` of `track_type` (OAKENGINE_TRACK_TYPE_*) is
+ * enabled (VideoParams/AudioParams/SubtitleParams::enabled()), else 0;
+ * OAKENGINE_E_INVALID (< 0) on bad arguments.
+ */
+OAKENGINE_API int oakengine_viewer_get_stream_enabled(
+ const OakEngineNode *self, int track_type, int index);
+
+/**
+ * @brief Number of subtitles in subtitle stream `index`
+ * (SubtitleParams::size()); < 0 on bad arguments.
+ */
+OAKENGINE_API int oakengine_viewer_get_subtitle_count(
+ const OakEngineNode *self, int index);
+
+/**
+ * @brief Borrowed pointer to subtitle `sub_index` of subtitle stream
+ * `index` (a const olive::Subtitle*; the application copies the value out,
+ * it must not free or store it beyond the footage's lifetime). NULL on
+ * bad arguments.
+ */
+OAKENGINE_API const void *oakengine_viewer_get_subtitle_at(
+ const OakEngineNode *self, int index, int sub_index);
+
+/**
+ * @brief 1 if the viewer has at least one enabled stream of `track_type`
+ * (OAKENGINE_TRACK_TYPE_* from timeline.h), else 0
+ * (ViewerOutput::has_enabled_video/audio/subtitle_streams()).
+ */
+OAKENGINE_API int oakengine_viewer_has_enabled_streams(
+ const OakEngineNode *self, int track_type);
+
+/**
+ * @brief Params of the first enabled video stream
+ * (ViewerOutput::get_first_enabled_video_stream()); zeroed struct when
+ * none is enabled.
+ */
+OAKENGINE_API int oakengine_viewer_get_first_enabled_video_stream(
+ const OakEngineNode *self, oak_video_params *out);
+
+/**
+ * @brief Number of enabled streams of all types
+ * (ViewerOutput::get_enabled_streams_as_references().size()).
+ */
+OAKENGINE_API int oakengine_viewer_get_enabled_stream_count(
+ const OakEngineNode *self);
+
+/**
+ * @brief Write the enabled stream references
+ * (ViewerOutput::get_enabled_streams_as_references()) into caller arrays:
+ * `types[k]` = OAKENGINE_TRACK_TYPE_*, `indices[k]` = stream index within
+ * that type. At most `max` entries are written; returns the total count
+ * (call with max=0/NULL arrays to query, or use
+ * oakengine_viewer_get_enabled_stream_count()).
+ */
+OAKENGINE_API int oakengine_viewer_get_enabled_streams(
+ const OakEngineNode *self, int *types, int *indices, int max);
+
+/* ---- Workarea -------------------------------------------------------------- */
+
+/** @brief Snapshot of the viewer's workarea (ViewerOutput::get_work_area()
+ * range/enabled as POD). */
+OAKENGINE_API int oakengine_viewer_get_workarea(
+ const OakEngineNode *self, oakengine_viewer_workarea *out);
+
+/** @brief Set the workarea range (TimelineWorkArea::set_range()). Emits the
+ * workarea range notification on the underlying workarea object. */
+OAKENGINE_API int oakengine_viewer_set_workarea_range(OakEngineNode *self,
+ int64_t in_num,
+ int64_t in_den,
+ int64_t out_num,
+ int64_t out_den);
+
+/** @brief Enable/disable the workarea (TimelineWorkArea::set_enabled()). */
+OAKENGINE_API int oakengine_viewer_set_workarea_enabled(OakEngineNode *self,
+ int enabled);
+
+/* ---- Parameter setup / waveform --------------------------------------------- */
+
+/** @brief Apply the application default parameters
+ * (ViewerOutput::set_default_parameters(): width/height/pixel aspect/
+ * interlacing/audio layout from Config, frame rate from
+ * DefaultSequenceFrameRate). */
+OAKENGINE_API int oakengine_viewer_set_default_parameters(OakEngineNode *self);
+
+/**
+ * @brief Create a command that sets the viewer's preview resolution divider
+ * (changes the k_video_params_input standard value). Returns an opaque command
+ * pointer, or NULL when `self` is not a viewer or `divider` is invalid.
+ */
+OAKENGINE_API void *oakengine_viewer_set_preview_divider_command(
+ OakEngineNode *self, int divider);
+
+/**
+ * @brief Adopt the parameters of the given footage viewers
+ * (ViewerOutput::set_parameters_from_footage()). Every element of
+ * `footage` must itself be a viewer handle.
+ */
+OAKENGINE_API int oakengine_viewer_set_parameters_from_footage(
+ OakEngineNode *self, OakEngineNode *const *footage, int count);
+
+/** @brief Enable/disable waveform cache requests
+ * (ViewerOutput::set_waveform_enabled()). */
+OAKENGINE_API int oakengine_viewer_set_waveform_enabled(OakEngineNode *self,
+ int enabled);
+
+/**
+ * @brief The waveform cache of the connected sample output, or NULL
+ * (ViewerOutput::get_connected_waveform()). Opaque borrowed pointer; the
+ * application only passes it through to its own audio monitor, it must not
+ * dereference it.
+ */
+OAKENGINE_API const void *
+oakengine_viewer_get_connected_waveform(const OakEngineNode *self);
+
+/**
+ * @brief Borrowed handle of the viewer's timeline marker list
+ * (ViewerOutput::get_markers()), for the oakengine_marker_list_* family
+ * and the OAKENGINE_EVENT_MARKER_LIST_* events. NULL when `self` is not a
+ * viewer.
+ */
+OAKENGINE_API OakEngineMarkerList *
+oakengine_viewer_get_marker_list(OakEngineNode *self);
+
+/**
+ * @brief Borrowed handle of the viewer's workarea
+ * (ViewerOutput::get_work_area()), for the oakengine_workarea_* family and
+ * the OAKENGINE_EVENT_WORKAREA_* events. NULL when `self` is not a viewer.
+ */
+OAKENGINE_API OakEngineWorkarea *
+oakengine_viewer_get_workarea_handle(OakEngineNode *self);
+
+/* ---- Playback cache / frame cache ------------------------------------------ */
+
+/**
+ * @brief Opaque playback cache handle (olive::PlaybackCache).
+ */
+typedef struct OakEnginePlaybackCache OakEnginePlaybackCache;
+
+/**
+ * @brief Opaque frame cache handle (olive::FrameHashCache).
+ */
+typedef struct OakEngineFrameCache OakEngineFrameCache;
+
+/**
+ * @brief Borrowed playback cache of a viewer's connected output
+ * (ViewerOutput::get_connected_video_cache() for video, or from the
+ * ClipBlock::connected_video_cache()). Returns NULL when not available
+ * or when `self` is not a viewer/clip node.
+ */
+OAKENGINE_API OakEnginePlaybackCache *
+oakengine_viewer_get_playback_cache(OakEngineNode *self);
+
+/**
+ * @brief Static indicator height for playback cache rendering
+ * (PlaybackCache::get_cache_indicator_height()). > 0.
+ */
+OAKENGINE_API int oakengine_playback_cache_indicator_height(void);
+
+/**
+ * @brief Fill `ranges` with the valid (cached) time ranges from the
+ * playback cache. `ranges` is an array of (in_num,in_den,out_num,out_den)
+ * int64_t quads; at most `max` ranges are written. Returns the number of
+ * ranges written, or OAKENGINE_E_INVALID on NULL cache.
+ */
+OAKENGINE_API int oakengine_playback_cache_valid_ranges(
+ OakEnginePlaybackCache *cache, int64_t *ranges, int max);
+
+/**
+ * @brief Borrowed frame hash cache (FrameHashCache) of a viewer node
+ * (ViewerOutput has a get_video_cache(), etc.). Returns NULL when not
+ * available or when `self` is not a viewer node.
+ */
+OAKENGINE_API OakEngineFrameCache *
+oakengine_viewer_get_frame_cache(OakEngineNode *self);
+
+#ifdef __cplusplus
+}
+#endif
+
+#endif /* OAKENGINE_VIEWER_H */
diff --git a/engine/include/oakengine/worker.h b/engine/include/oakengine/worker.h
new file mode 100644
index 000000000..e1f345a11
--- /dev/null
+++ b/engine/include/oakengine/worker.h
@@ -0,0 +1,146 @@
+/***
+
+ 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 .
+
+***/
+
+#ifndef OAKENGINE_WORKER_H
+#define OAKENGINE_WORKER_H
+
+#include "export.h"
+
+#ifdef __cplusplus
+extern "C" {
+#endif
+
+/**
+ * @file worker.h
+ * @brief C ABI for the render worker process logic
+ *
+ * The render worker (oak-render-worker) is a headless render process spawned
+ * by the editor through the render worker pool. All of its runtime logic —
+ * Qt application setup, render backend initialization, the startup handshake
+ * and the NDJSON control-message loop — lives inside liboakengine behind this
+ * pure C interface, so the worker executable itself contains no engine C++
+ * ABI usage.
+ *
+ * Two entry levels are exposed:
+ *
+ * - oakengine_worker_main(): a drop-in main() for the worker executable.
+ * It creates the QGuiApplication, parses --backend, initializes the
+ * renderer, sends the startup handshake and runs the stdin/stdout NDJSON
+ * loop until a shutdown message or EOF.
+ *
+ * - The OakWorkerSession family: the same message-handling state machine
+ * in a transport-agnostic form, so tests (and alternative transports)
+ * can drive it line by line without spawning a process. Responses that
+ * the worker would write to stdout are returned through the buf/size
+ * convention instead.
+ *
+ * Conventions (mirrors ipc.h):
+ * - Returned handles are owned by the caller and must be released with the
+ * matching _free(). NULL is accepted by every function and yields a
+ * no-op / zero result.
+ * - String output uses the buf/size convention: the return value is the
+ * number of characters that would have been written excluding the NUL,
+ * so buf == NULL or a short buffer queries the required size. The output
+ * is NUL-terminated whenever buf_size > 0.
+ */
+
+typedef struct OakWorkerSession OakWorkerSession;
+
+/**
+ * @brief Create a worker session for the given render backend.
+ *
+ * `backend` names the render backend ("opengl", "vulkan"); the session tries
+ * the dynamic backend first and falls back to the direct OpenGL renderer,
+ * exactly like the worker main. NULL, "" or "none" skips renderer creation
+ * entirely, producing a session that can parse and answer control messages
+ * but cannot actually render (useful for exercising error paths in tests).
+ *
+ * A QGuiApplication must exist before creating a session with a real
+ * backend. The returned handle is owned by the caller.
+ */
+OAKENGINE_API OakWorkerSession *
+oakengine_worker_session_create(const char *backend);
+
+OAKENGINE_API void oakengine_worker_session_free(OakWorkerSession *self);
+
+/**
+ * @brief 1 if the session holds a successfully initialized render backend.
+ */
+OAKENGINE_API int
+oakengine_worker_session_has_renderer(const OakWorkerSession *self);
+
+/**
+ * @brief Load the engine runtime services the session depends on (config,
+ * node factory, color manager, frame/disk managers, project serializer).
+ *
+ * Idempotent in practice: the underlying services are process-wide
+ * singletons. Returns 1 on success, 0 on failure (NULL session).
+ */
+OAKENGINE_API int
+oakengine_worker_session_initialize_runtime(OakWorkerSession *self);
+
+/**
+ * @brief Build the startup handshake the worker sends to its parent
+ * (buf/size convention).
+ *
+ * Announces the protocol version and, when a renderer is present, the
+ * negotiated GL version. Returns the required size, or -1 on failure.
+ */
+OAKENGINE_API int
+oakengine_worker_session_startup_handshake(OakWorkerSession *self, char *buf,
+ int buf_size);
+
+/**
+ * @brief Handle one NDJSON control line and produce the response, if any.
+ *
+ * `line` is one complete JSON message (with or without the trailing
+ * newline). The response — what the worker main loop would write to stdout —
+ * is serialized into response_buf using the buf/size convention: the return
+ * value is the number of characters that would have been written excluding
+ * the NUL, so 0 means "no response" (e.g. a successful handshake or a
+ * shutdown message) and a positive value queries/fills the response. A
+ * malformed `line` yields an error response, not a failure.
+ *
+ * Returns -1 when the handler itself failed (the worker main treats this as
+ * a fatal error for its exit code, though it keeps draining input).
+ */
+OAKENGINE_API int
+oakengine_worker_session_handle_json(OakWorkerSession *self, const char *line,
+ char *response_buf, int response_buf_size);
+
+/**
+ * @brief 1 once a shutdown control message has been received.
+ */
+OAKENGINE_API int
+oakengine_worker_session_shutdown_requested(const OakWorkerSession *self);
+
+/**
+ * @brief Full render-worker main(). `argc`/`argv` are passed through from
+ * the executable's main; "--backend " selects the render backend.
+ *
+ * Returns the process exit code (0 on clean shutdown).
+ */
+OAKENGINE_API int oakengine_worker_main(int argc, char **argv);
+
+#ifdef __cplusplus
+}
+#endif
+
+#endif /* OAKENGINE_WORKER_H */
diff --git a/engine/src/capi/CMakeLists.txt b/engine/src/capi/CMakeLists.txt
index 29175dba8..9175f7ee3 100644
--- a/engine/src/capi/CMakeLists.txt
+++ b/engine/src/capi/CMakeLists.txt
@@ -23,22 +23,61 @@
set(OLIVE_SOURCES
${OLIVE_SOURCES}
include/oakengine/init.h
+ include/oakengine/events.h
+ include/oakengine/app.h
include/oakengine/project.h
include/oakengine/timeline.h
include/oakengine/renderer.h
+ include/oakengine/display.h
include/oakengine/footage.h
include/oakengine/exporter.h
+ include/oakengine/encoding.h
+ include/oakengine/videoparams.h
+ include/oakengine/color.h
include/oakengine/node.h
include/oakengine/playback.h
include/oakengine/preview.h
+ include/oakengine/sync.h
+ include/oakengine/worker.h
+ include/oakengine/viewer.h
+ include/oakengine/traverse.h
+ include/oakengine/task.h
+ include/oakengine/undo.h
+ include/oakengine/config.h
+ include/oakengine/audio.h
+ include/oakengine/disk.h
+ include/oakengine/proxy.h
+ include/oakengine/lut.h
+ include/oakengine/serializer.h
+ include/oakengine/plugin.h
+ include/oakengine/gizmo.h
src/capi/init.cpp
+ src/capi/events.cpp
+ src/capi/app.cpp
src/capi/project.cpp
src/capi/timeline.cpp
src/capi/renderer.cpp
+ src/capi/display.cpp
src/capi/footage.cpp
src/capi/export.cpp
+ src/capi/encoding.cpp
+ src/capi/color.cpp
src/capi/node.cpp
src/capi/playback.cpp
src/capi/preview.cpp
+ src/capi/sync.cpp
+ src/capi/worker.cpp
+ src/capi/viewer.cpp
+ src/capi/traverse.cpp
+ src/capi/task.cpp
+ src/capi/undo.cpp
+ src/capi/config.cpp
+ src/capi/audio.cpp
+ src/capi/disk.cpp
+ src/capi/proxy.cpp
+ src/capi/lut.cpp
+ src/capi/serializer.cpp
+ src/capi/plugin.cpp
+ src/capi/gizmo.cpp
PARENT_SCOPE
)
diff --git a/engine/src/capi/app.cpp b/engine/src/capi/app.cpp
new file mode 100644
index 000000000..b1b8a61ed
--- /dev/null
+++ b/engine/src/capi/app.cpp
@@ -0,0 +1,812 @@
+/***
+
+ 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 .
+
+***/
+
+#include "oakengine/app.h"
+
+#include
+#include
+
+#include
+#include
+#include
+
+#include "coreengine.h"
+#include "node/project.h"
+#include "node/project/footage/footage.h"
+#include "node/project/sequence/sequence.h"
+#include "node/project/serializer/serializedlayoutinfo.h"
+#include "task/task.h"
+#include "undo/undostack.h"
+
+namespace
+{
+
+olive::Project *impl(OakEngineProject *h)
+{
+ return reinterpret_cast(h);
+}
+
+OakEngineProject *wrap(olive::Project *p)
+{
+ return reinterpret_cast(p);
+}
+
+OakEngineSequence *wrap_seq(olive::Sequence *s)
+{
+ return reinterpret_cast(s);
+}
+
+// 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());
+}
+
+// Registered callback set (all fields may be null).
+OakEngineAppCallbacks g_callbacks = {};
+
+// Whether oakengine_app_start() has run (and oakengine_app_stop() has not).
+bool g_started = false;
+
+// The EngineCore the notification signals are currently connected to.
+olive::EngineCore *g_connected_core = nullptr;
+
+olive::EngineCore *app_core()
+{
+ return olive::EngineCore::instance();
+}
+
+// The EngineCore constructor's UndoStack member creates QActions, which need
+// QGuiApplication state (same reason as oakengine_init()).
+void ensure_qcoreapplication()
+{
+ if (QCoreApplication::instance()) {
+ return;
+ }
+
+ if (qEnvironmentVariableIsEmpty("QT_QPA_PLATFORM")) {
+ qputenv("QT_QPA_PLATFORM", "offscreen");
+ }
+
+ static int argc = 1;
+ static char app_name[] = "oakengine";
+ static char *argv[] = { app_name, nullptr };
+ new QGuiApplication(argc, argv);
+
+ QCoreApplication::setOrganizationName(QStringLiteral("oakvideoeditor.org"));
+ QCoreApplication::setApplicationName(QStringLiteral("Oak Video Editor"));
+}
+
+// Forward engine signals to the registered C callbacks. Connected once per
+// EngineCore instance; dropped events are fine while no callback is set.
+void connect_notifications(olive::EngineCore *core)
+{
+ if (!core || g_connected_core == core) {
+ return;
+ }
+ g_connected_core = core;
+
+ QObject::connect(core, &olive::EngineCore::status_message_show, core,
+ [](const QString &message, int timeout) {
+ if (g_callbacks.status_message_show) {
+ g_callbacks.status_message_show(
+ message.toUtf8().constData(), timeout,
+ g_callbacks.userdata);
+ }
+ });
+ QObject::connect(core, &olive::EngineCore::status_message_clear, core,
+ [] {
+ if (g_callbacks.status_message_clear) {
+ g_callbacks.status_message_clear(
+ g_callbacks.userdata);
+ }
+ });
+ QObject::connect(core, &olive::EngineCore::cache_full_warning_requested,
+ core, [] {
+ if (g_callbacks.cache_full_warning) {
+ g_callbacks.cache_full_warning(
+ g_callbacks.userdata);
+ }
+ });
+ QObject::connect(core, &olive::EngineCore::active_project_changed, core,
+ [](olive::Project *p) {
+ if (g_callbacks.active_project_changed) {
+ g_callbacks.active_project_changed(
+ wrap(p), g_callbacks.userdata);
+ }
+ });
+ QObject::connect(core, &olive::EngineCore::tool_changed, core,
+ [](const olive::Tool::Item &tool) {
+ if (g_callbacks.tool_changed) {
+ g_callbacks.tool_changed(int(tool),
+ g_callbacks.userdata);
+ }
+ });
+ QObject::connect(core, &olive::EngineCore::addable_object_changed, core,
+ [](olive::Tool::AddableObject o) {
+ if (g_callbacks.addable_object_changed) {
+ g_callbacks.addable_object_changed(
+ int(o), g_callbacks.userdata);
+ }
+ });
+ QObject::connect(core, &olive::EngineCore::snapping_changed, core,
+ [](const bool &b) {
+ if (g_callbacks.snapping_changed) {
+ g_callbacks.snapping_changed(b ? 1 : 0,
+ g_callbacks.userdata);
+ }
+ });
+ QObject::connect(core, &olive::EngineCore::timecode_display_changed, core,
+ [](olive::core::Timecode::Display d) {
+ if (g_callbacks.timecode_display_changed) {
+ g_callbacks.timecode_display_changed(
+ int(d), g_callbacks.userdata);
+ }
+ });
+ QObject::connect(core, &olive::EngineCore::open_recent_list_changed, core,
+ [] {
+ if (g_callbacks.open_recent_list_changed) {
+ g_callbacks.open_recent_list_changed(
+ g_callbacks.userdata);
+ }
+ });
+ QObject::connect(core, &olive::EngineCore::color_picker_enabled, core,
+ [](bool e) {
+ if (g_callbacks.color_picker_enabled) {
+ g_callbacks.color_picker_enabled(
+ e ? 1 : 0, g_callbacks.userdata);
+ }
+ });
+}
+
+// Translate the C handler callbacks into the std::function handlers
+// EngineCore calls when it needs user interaction.
+void install_handlers(olive::EngineCore *core)
+{
+ if (g_callbacks.confirm_image_sequence) {
+ core->set_confirm_image_sequence_handler([](const QString &filename) {
+ return g_callbacks.confirm_image_sequence(
+ filename.toUtf8().constData(),
+ g_callbacks.userdata) != 0;
+ });
+ } else {
+ core->set_confirm_image_sequence_handler(nullptr);
+ }
+
+ if (g_callbacks.relink_footage) {
+ core->set_relink_handler([](QVector footage) {
+ return g_callbacks.relink_footage(
+ reinterpret_cast(footage.data()),
+ int(footage.size()), g_callbacks.userdata) != 0;
+ });
+ } else {
+ core->set_relink_handler(nullptr);
+ }
+
+ if (g_callbacks.save_project) {
+ core->set_save_project_handler([](const QString &override_filename) {
+ g_callbacks.save_project(override_filename.toUtf8().constData(),
+ g_callbacks.userdata);
+ });
+ } else {
+ core->set_save_project_handler(nullptr);
+ }
+
+ if (g_callbacks.close_project) {
+ core->set_close_project_handler([] {
+ return g_callbacks.close_project(g_callbacks.userdata) != 0;
+ });
+ } else {
+ core->set_close_project_handler(nullptr);
+ }
+
+ if (g_callbacks.load_layout) {
+ core->set_load_layout_handler(
+ [](const olive::SerializedLayoutInfo &layout) {
+ g_callbacks.load_layout(&layout, g_callbacks.userdata);
+ });
+ } else {
+ core->set_load_layout_handler(nullptr);
+ }
+
+#ifdef USE_OTIO
+ if (g_callbacks.otio_import) {
+ core->set_otio_import_handler(
+ [](const QList &sequences) {
+ QVector handles;
+ handles.reserve(sequences.size());
+ for (olive::Sequence *s : sequences) {
+ handles.append(wrap_seq(s));
+ }
+ return g_callbacks.otio_import(handles.data(),
+ int(handles.size()),
+ g_callbacks.userdata) != 0;
+ });
+ } else {
+ core->set_otio_import_handler(nullptr);
+ }
+#endif
+}
+
+} // namespace
+
+extern "C"
+{
+
+int oakengine_app_create(const OakEngineAppParams *params)
+{
+ if (app_core()) {
+ return OAKENGINE_E_STATE;
+ }
+
+ ensure_qcoreapplication();
+
+ olive::EngineCore::CoreParams core_params;
+ if (params) {
+ switch (params->run_mode) {
+ case OAKENGINE_APP_RUN_HEADLESS_EXPORT:
+ core_params.set_run_mode(
+ olive::EngineCore::CoreParams::k_headless_export);
+ break;
+ case OAKENGINE_APP_RUN_HEADLESS_PRE_CACHE:
+ core_params.set_run_mode(
+ olive::EngineCore::CoreParams::k_headless_pre_cache);
+ break;
+ default:
+ core_params.set_run_mode(
+ olive::EngineCore::CoreParams::k_run_normal);
+ break;
+ }
+ core_params.set_fullscreen(params->fullscreen != 0);
+ if (params->startup_project) {
+ core_params.set_startup_project(
+ QString::fromUtf8(params->startup_project));
+ }
+ if (params->startup_language) {
+ core_params.set_startup_language(
+ QString::fromUtf8(params->startup_language));
+ }
+ if (params->crash_on_startup) {
+ core_params.set_crash_on_startup(true);
+ }
+ }
+
+ // Never deleted: backs the process-wide EngineCore singleton (same
+ // lifetime rule as the oakengine_init() shell).
+ new olive::EngineCore(core_params);
+
+ return OAKENGINE_OK;
+}
+
+int oakengine_app_start(void)
+{
+ if (!app_core() || g_started) {
+ return OAKENGINE_E_STATE;
+ }
+
+ app_core()->start();
+ g_started = true;
+ return OAKENGINE_OK;
+}
+
+int oakengine_app_stop(void)
+{
+ if (!app_core() || !g_started) {
+ return OAKENGINE_E_STATE;
+ }
+
+ app_core()->stop();
+ g_started = false;
+ return OAKENGINE_OK;
+}
+
+int oakengine_app_set_callbacks(const OakEngineAppCallbacks *callbacks)
+{
+ if (callbacks) {
+ g_callbacks = *callbacks;
+ } else {
+ g_callbacks = OakEngineAppCallbacks{};
+ }
+
+ if (olive::EngineCore *core = app_core()) {
+ connect_notifications(core);
+ install_handlers(core);
+ }
+
+ return OAKENGINE_OK;
+}
+
+int oakengine_app_run_mode(void)
+{
+ if (!app_core()) {
+ return OAKENGINE_E_INVALID;
+ }
+
+ switch (app_core()->core_params().run_mode()) {
+ case olive::EngineCore::CoreParams::k_headless_export:
+ return OAKENGINE_APP_RUN_HEADLESS_EXPORT;
+ case olive::EngineCore::CoreParams::k_headless_pre_cache:
+ return OAKENGINE_APP_RUN_HEADLESS_PRE_CACHE;
+ default:
+ return OAKENGINE_APP_RUN_NORMAL;
+ }
+}
+
+int oakengine_app_fullscreen(void)
+{
+ if (!app_core()) {
+ return OAKENGINE_E_INVALID;
+ }
+ return app_core()->core_params().fullscreen() ? 1 : 0;
+}
+
+int oakengine_app_startup_project(char *buf, int buf_size)
+{
+ if (!app_core()) {
+ return OAKENGINE_E_INVALID;
+ }
+ return string_to_buf(app_core()->core_params().startup_project(), buf,
+ buf_size);
+}
+
+void *oakengine_app_undo_stack(void)
+{
+ if (!app_core()) {
+ return nullptr;
+ }
+ return app_core()->undo_stack();
+}
+
+int oakengine_app_tool(void)
+{
+ if (!app_core()) {
+ return OAKENGINE_E_INVALID;
+ }
+ return int(app_core()->tool());
+}
+
+int oakengine_app_set_tool(int tool)
+{
+ if (!app_core()) {
+ return OAKENGINE_E_INVALID;
+ }
+ if (tool < 0 || tool >= int(olive::Tool::k_count)) {
+ return OAKENGINE_E_INVALID;
+ }
+
+ app_core()->set_tool(static_cast(tool));
+ return OAKENGINE_OK;
+}
+
+int oakengine_app_addable_object(void)
+{
+ if (!app_core()) {
+ return OAKENGINE_E_INVALID;
+ }
+ return int(app_core()->get_selected_addable_object());
+}
+
+int oakengine_app_set_addable_object(int object)
+{
+ if (!app_core()) {
+ return OAKENGINE_E_INVALID;
+ }
+ if (object < 0 || object >= int(olive::Tool::k_addable_count)) {
+ return OAKENGINE_E_INVALID;
+ }
+
+ app_core()->set_selected_addable_object(
+ static_cast(object));
+ return OAKENGINE_OK;
+}
+
+int oakengine_app_selected_transition(char *buf, int buf_size)
+{
+ if (!app_core()) {
+ return OAKENGINE_E_INVALID;
+ }
+ return string_to_buf(app_core()->get_selected_transition(), buf, buf_size);
+}
+
+int oakengine_app_set_selected_transition(const char *id)
+{
+ if (!app_core()) {
+ return OAKENGINE_E_INVALID;
+ }
+
+ app_core()->set_selected_transition_object(
+ id ? QString::fromUtf8(id) : QString());
+ return OAKENGINE_OK;
+}
+
+int oakengine_app_snapping(void)
+{
+ if (!app_core()) {
+ return OAKENGINE_E_INVALID;
+ }
+ return app_core()->snapping() ? 1 : 0;
+}
+
+int oakengine_app_set_snapping(int enabled)
+{
+ if (!app_core()) {
+ return OAKENGINE_E_INVALID;
+ }
+
+ app_core()->set_snapping(enabled != 0);
+ return OAKENGINE_OK;
+}
+
+int oakengine_app_timecode_display(void)
+{
+ if (!app_core()) {
+ return OAKENGINE_E_INVALID;
+ }
+ return int(app_core()->get_timecode_display());
+}
+
+int oakengine_app_set_timecode_display(int display)
+{
+ if (!app_core()) {
+ return OAKENGINE_E_INVALID;
+ }
+ if (display < 0 ||
+ display > int(olive::core::Timecode::k_milliseconds)) {
+ return OAKENGINE_E_INVALID;
+ }
+
+ app_core()->set_timecode_display(
+ static_cast(display));
+ return OAKENGINE_OK;
+}
+
+int oakengine_app_recent_projects_count(void)
+{
+ if (!app_core()) {
+ return OAKENGINE_E_INVALID;
+ }
+ return int(app_core()->get_recent_projects().size());
+}
+
+int oakengine_app_recent_project_at(int index, char *buf, int buf_size)
+{
+ if (!app_core()) {
+ return OAKENGINE_E_INVALID;
+ }
+ const QStringList &recent = app_core()->get_recent_projects();
+ if (index < 0 || index >= recent.size()) {
+ return OAKENGINE_E_NOT_FOUND;
+ }
+ return string_to_buf(recent.at(index), buf, buf_size);
+}
+
+int oakengine_app_remove_recent_project(int index)
+{
+ if (!app_core()) {
+ return OAKENGINE_E_INVALID;
+ }
+ if (index < 0 || index >= app_core()->get_recent_projects().size()) {
+ return OAKENGINE_E_NOT_FOUND;
+ }
+
+ app_core()->remove_recently_opened_project(index);
+ return OAKENGINE_OK;
+}
+
+int oakengine_app_clear_recent_projects(void)
+{
+ if (!app_core()) {
+ return OAKENGINE_E_INVALID;
+ }
+
+ app_core()->clear_open_recent_list();
+ return OAKENGINE_OK;
+}
+
+int oakengine_app_show_status_message(const char *message, int timeout)
+{
+ if (!app_core() || !message) {
+ return OAKENGINE_E_INVALID;
+ }
+
+ app_core()->show_status_bar_message(QString::fromUtf8(message), timeout);
+ return OAKENGINE_OK;
+}
+
+int oakengine_app_clear_status_message(void)
+{
+ if (!app_core()) {
+ return OAKENGINE_E_INVALID;
+ }
+
+ app_core()->clear_status_bar_message();
+ return OAKENGINE_OK;
+}
+
+int oakengine_app_set_language(const char *locale)
+{
+ if (!app_core() || !locale) {
+ return OAKENGINE_E_INVALID;
+ }
+
+ return app_core()->set_language(QString::fromUtf8(locale)) ? 1 : 0;
+}
+
+int oakengine_app_set_autorecovery_interval(int minutes)
+{
+ if (!app_core()) {
+ return OAKENGINE_E_INVALID;
+ }
+
+ app_core()->set_autorecovery_interval(minutes);
+ return OAKENGINE_OK;
+}
+
+int oakengine_app_set_use_proxy_media(int enabled)
+{
+ if (!app_core()) {
+ return OAKENGINE_E_INVALID;
+ }
+
+ app_core()->set_use_proxy_media(enabled != 0);
+ return OAKENGINE_OK;
+}
+
+int oakengine_app_request_pixel_sampling(int enable)
+{
+ if (!app_core()) {
+ return OAKENGINE_E_INVALID;
+ }
+
+ app_core()->request_pixel_sampling_in_viewers(enable != 0);
+ return OAKENGINE_OK;
+}
+
+int oakengine_app_set_magic(int enabled)
+{
+ if (!app_core()) {
+ return OAKENGINE_E_INVALID;
+ }
+
+ app_core()->set_magic(enabled != 0);
+ return OAKENGINE_OK;
+}
+
+int oakengine_app_is_magic_enabled(void)
+{
+ if (!app_core()) {
+ return OAKENGINE_E_INVALID;
+ }
+ return app_core()->is_magic_enabled() ? 1 : 0;
+}
+
+int oakengine_app_copy_to_clipboard(const char *text)
+{
+ if (!app_core() || !text) {
+ return OAKENGINE_E_INVALID;
+ }
+
+ olive::EngineCore::copy_string_to_clipboard(QString::fromUtf8(text));
+ return OAKENGINE_OK;
+}
+
+int oakengine_app_paste_from_clipboard(char *buf, int buf_size)
+{
+ if (!app_core()) {
+ return OAKENGINE_E_INVALID;
+ }
+ return string_to_buf(olive::EngineCore::paste_string_from_clipboard(), buf,
+ buf_size);
+}
+
+int oakengine_app_footage_file_dialog_filter(char *buf, int buf_size)
+{
+ return string_to_buf(olive::EngineCore::footage_file_dialog_filter(), buf,
+ buf_size);
+}
+
+int oakengine_app_is_footage_extension_allowed(const char *path)
+{
+ if (!path) {
+ return OAKENGINE_E_INVALID;
+ }
+ return olive::EngineCore::is_footage_extension_allowed(
+ QString::fromUtf8(path)) ?
+ 1 :
+ 0;
+}
+
+OakEngineSequence *oakengine_app_create_sequence(OakEngineProject *project,
+ const char *name_format)
+{
+ if (!app_core() || !project) {
+ return nullptr;
+ }
+
+ const QString format = name_format ?
+ QString::fromUtf8(name_format) :
+ QStringLiteral("Sequence %1");
+ return wrap_seq(olive::EngineCore::create_new_sequence_for_project(
+ format, impl(project)));
+}
+
+int oakengine_app_auto_recovery_index_filename(char *buf, int buf_size)
+{
+ if (!app_core()) {
+ return OAKENGINE_E_INVALID;
+ }
+ return string_to_buf(olive::EngineCore::get_auto_recovery_index_filename(),
+ buf, buf_size);
+}
+
+OakEngineProject *oakengine_app_open_project(void)
+{
+ if (!app_core()) {
+ return nullptr;
+ }
+ return wrap(app_core()->open_project());
+}
+
+int oakengine_app_create_new_project(void)
+{
+ if (!app_core()) {
+ return OAKENGINE_E_INVALID;
+ }
+
+ app_core()->create_new_project();
+ return OAKENGINE_OK;
+}
+
+int oakengine_app_add_open_project(OakEngineProject *project,
+ int add_to_recents)
+{
+ if (!app_core() || !project) {
+ return OAKENGINE_E_INVALID;
+ }
+
+ app_core()->add_open_project(impl(project), add_to_recents != 0);
+ return OAKENGINE_OK;
+}
+
+int oakengine_app_add_open_project_from_task(void *task, int add_to_recents)
+{
+ if (!app_core() || !task) {
+ return OAKENGINE_E_INVALID;
+ }
+
+ return app_core()->add_open_project_from_task(
+ static_cast(task), add_to_recents != 0) ?
+ 1 :
+ 0;
+}
+
+int oakengine_app_add_recovery_project_from_task(void *task)
+{
+ if (!app_core() || !task) {
+ return OAKENGINE_E_INVALID;
+ }
+
+ app_core()->add_recovery_project_from_task(static_cast(task));
+ return OAKENGINE_OK;
+}
+
+int oakengine_app_on_project_saved(OakEngineProject *project)
+{
+ if (!app_core() || !project) {
+ return OAKENGINE_E_INVALID;
+ }
+
+ app_core()->on_project_saved(impl(project));
+ return OAKENGINE_OK;
+}
+
+int oakengine_app_set_active_project(OakEngineProject *project)
+{
+ if (!app_core()) {
+ return OAKENGINE_E_INVALID;
+ }
+
+ app_core()->set_active_project(impl(project));
+ return OAKENGINE_OK;
+}
+
+// ---- Individual handler setter convenience wrappers ----
+
+int oakengine_app_set_confirm_image_sequence_handler(
+ int (*fn)(const char *filename, void *userdata), void *userdata)
+{
+ OakEngineAppCallbacks cb = g_callbacks;
+ cb.userdata = userdata;
+ cb.confirm_image_sequence = fn;
+ return oakengine_app_set_callbacks(&cb);
+}
+
+int oakengine_app_set_relink_handler(
+ int (*fn)(OakEngineFootage **footage, int count, void *userdata),
+ void *userdata)
+{
+ OakEngineAppCallbacks cb = g_callbacks;
+ cb.userdata = userdata;
+ cb.relink_footage = fn;
+ return oakengine_app_set_callbacks(&cb);
+}
+
+int oakengine_app_set_save_project_handler(
+ void (*fn)(const char *override_filename, void *userdata), void *userdata)
+{
+ OakEngineAppCallbacks cb = g_callbacks;
+ cb.userdata = userdata;
+ cb.save_project = fn;
+ return oakengine_app_set_callbacks(&cb);
+}
+
+int oakengine_app_set_close_project_handler(
+ int (*fn)(void *userdata), void *userdata)
+{
+ OakEngineAppCallbacks cb = g_callbacks;
+ cb.userdata = userdata;
+ cb.close_project = fn;
+ return oakengine_app_set_callbacks(&cb);
+}
+
+int oakengine_app_set_load_layout_handler(
+ void (*fn)(const void *layout, void *userdata), void *userdata)
+{
+ OakEngineAppCallbacks cb = g_callbacks;
+ cb.userdata = userdata;
+ cb.load_layout = fn;
+ return oakengine_app_set_callbacks(&cb);
+}
+
+// ---- void*-based convenience overloads ----
+
+int oakengine_app_get_auto_recovery_index_filename(char *buf, int buf_size)
+{
+ return oakengine_app_auto_recovery_index_filename(buf, buf_size);
+}
+
+int oakengine_app_remove_recently_opened_project(int index)
+{
+ return oakengine_app_remove_recent_project(index);
+}
+
+int oakengine_app_on_project_saved_vp(void *project)
+{
+ return oakengine_app_on_project_saved(
+ reinterpret_cast(project));
+}
+
+int oakengine_app_set_active_project_vp(void *project)
+{
+ return oakengine_app_set_active_project(
+ reinterpret_cast(project));
+}
+
+int oakengine_app_add_open_project_vp(void *project, int add_to_recents)
+{
+ return oakengine_app_add_open_project(
+ reinterpret_cast(project), add_to_recents);
+}
+
+} // extern "C"
diff --git a/engine/src/capi/audio.cpp b/engine/src/capi/audio.cpp
new file mode 100644
index 000000000..364e1f151
--- /dev/null
+++ b/engine/src/capi/audio.cpp
@@ -0,0 +1,458 @@
+/***
+
+ 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 .
+
+***/
+
+#include "oakengine/audio.h"
+#include "oakengine/encoding.h"
+
+#include
+#include
+#include
+
+#include
+#include
+
+#include "audio/audiomanager.h"
+#include "audio/audioprocessor.h"
+#include "audio/audiosynchronizer.h"
+#include "audio/audiowaveformsync.h"
+#include "olive/core/oakcore/audioparams.h"
+#include "olive/core/render/audioparams.h"
+
+namespace
+{
+
+int write_string(const QString &s, char *buf, int buf_size)
+{
+ const QByteArray utf8 = s.toUtf8();
+ const int len = int(utf8.size());
+ if (buf && buf_size > 0) {
+ const int n = qMin(len, buf_size - 1);
+ std::memcpy(buf, utf8.constData(), size_t(n));
+ buf[n] = '\0';
+ }
+ return len;
+}
+
+olive::AudioManager *manager()
+{
+ return olive::AudioManager::instance();
+}
+
+} // namespace
+
+extern "C" int oakengine_audio_create_instance(void)
+{
+ olive::AudioManager::create_instance();
+ return manager() ? OAKENGINE_OK : OAKENGINE_E_FAILED;
+}
+
+extern "C" int oakengine_audio_destroy_instance(void)
+{
+ olive::AudioManager::destroy_instance();
+ return OAKENGINE_OK;
+}
+
+extern "C" void *oakengine_audio_manager_handle(void)
+{
+ return manager();
+}
+
+extern "C" int64_t oakengine_audio_get_output_device(void)
+{
+ if (olive::AudioManager *m = manager()) {
+ return static_cast(m->get_output_device());
+ }
+ return -1; // paNoDevice
+}
+
+extern "C" int oakengine_audio_set_output_device(int64_t device)
+{
+ if (olive::AudioManager *m = manager()) {
+ m->set_output_device(static_cast(device));
+ return OAKENGINE_OK;
+ }
+ return OAKENGINE_E_STATE;
+}
+
+extern "C" int64_t oakengine_audio_get_input_device(void)
+{
+ if (olive::AudioManager *m = manager()) {
+ return static_cast(m->get_input_device());
+ }
+ return -1; // paNoDevice
+}
+
+extern "C" int oakengine_audio_set_input_device(int64_t device)
+{
+ if (olive::AudioManager *m = manager()) {
+ m->set_input_device(static_cast(device));
+ return OAKENGINE_OK;
+ }
+ return OAKENGINE_E_STATE;
+}
+
+extern "C" int oakengine_audio_hard_reset(void)
+{
+ if (olive::AudioManager *m = manager()) {
+ m->hard_reset();
+ return OAKENGINE_OK;
+ }
+ return OAKENGINE_E_STATE;
+}
+
+extern "C" int oakengine_audio_clear_buffered_output(void)
+{
+ if (olive::AudioManager *m = manager()) {
+ m->clear_buffered_output();
+ return OAKENGINE_OK;
+ }
+ return OAKENGINE_E_STATE;
+}
+
+extern "C" int oakengine_audio_push_to_output(const OakAudioParams *params,
+ const char *samples,
+ int64_t samples_size,
+ char *error_buf,
+ int error_buf_size)
+{
+ if (!params || !samples || samples_size < 0) {
+ return OAKENGINE_E_INVALID;
+ }
+
+ olive::AudioManager *m = manager();
+ if (!m) {
+ return OAKENGINE_E_STATE;
+ }
+
+ // The C++ AudioParams wrapper owns the OakAudioParams handle; the caller
+ // keeps ownership of `params`, so copy before wrapping.
+ const olive::core::AudioParams cpp_params =
+ olive::core::AudioParams::from_handle(
+ oakcore_audioparams_copy(params));
+
+ QString error;
+ const QByteArray data = QByteArray::fromRawData(samples,
+ static_cast(samples_size));
+ if (!m->push_to_output(cpp_params, data, &error)) {
+ write_string(error, error_buf, error_buf_size);
+ return OAKENGINE_E_FAILED;
+ }
+ return OAKENGINE_OK;
+}
+
+extern "C" int oakengine_audio_stop_recording(void)
+{
+ if (olive::AudioManager *m = manager()) {
+ m->stop_recording();
+ return OAKENGINE_OK;
+ }
+ return OAKENGINE_E_STATE;
+}
+
+extern "C" int oakengine_audio_stop_output(void)
+{
+ if (olive::AudioManager *m = manager()) {
+ m->stop_output();
+ return OAKENGINE_OK;
+ }
+ return OAKENGINE_E_STATE;
+}
+
+extern "C" int oakengine_audio_reset_output_clock(void)
+{
+ if (olive::AudioManager *m = manager()) {
+ m->reset_output_clock();
+ return OAKENGINE_OK;
+ }
+ return OAKENGINE_E_STATE;
+}
+
+extern "C" int oakengine_audio_set_output_notify_interval(int64_t bytes)
+{
+ if (olive::AudioManager *m = manager()) {
+ m->set_output_notify_interval(static_cast(bytes));
+ return OAKENGINE_OK;
+ }
+ return OAKENGINE_E_STATE;
+}
+
+extern "C" int oakengine_audio_start_recording(
+ OakEngineEncodingParams *params, char *error_buf, int error_buf_size)
+{
+ // Delegate to the encoding-family implementation which handles the
+ // OakEngineEncodingParams -> EncodingParams conversion internally.
+ return oakengine_encoding_start_audio_recording(params, error_buf,
+ error_buf_size);
+}
+
+
+extern "C" int oakengine_audio_estimate_envelope_offset(
+ const double *reference, int reference_len,
+ const double *candidate, int candidate_len,
+ const bool *reference_valid, int reference_valid_len,
+ const bool *candidate_valid, int candidate_valid_len,
+ uint64_t window_samples, int64_t max_offset_windows,
+ oak_audio_waveform_offset *out)
+{
+ if (!out || !reference || !candidate || reference_len < 0 ||
+ candidate_len < 0 || !window_samples) {
+ return OAKENGINE_E_INVALID;
+ }
+
+ if ((reference_valid && reference_valid_len != reference_len) ||
+ (candidate_valid && candidate_valid_len != candidate_len)) {
+ return OAKENGINE_E_INVALID;
+ }
+
+ QVector ref(reference_len);
+ std::copy(reference, reference + reference_len, ref.begin());
+ QVector cand(candidate_len);
+ std::copy(candidate, candidate + candidate_len, cand.begin());
+
+ QVector ref_valid;
+ if (reference_valid) {
+ ref_valid.resize(reference_valid_len);
+ std::copy(reference_valid, reference_valid + reference_valid_len,
+ ref_valid.begin());
+ }
+
+ QVector cand_valid;
+ if (candidate_valid) {
+ cand_valid.resize(candidate_valid_len);
+ std::copy(candidate_valid, candidate_valid + candidate_valid_len,
+ cand_valid.begin());
+ }
+
+ const olive::AudioWaveformSync::OffsetResult result =
+ olive::AudioWaveformSync::estimate_envelope_offset(
+ ref, cand, ref_valid, cand_valid, window_samples, max_offset_windows);
+
+ out->offset_samples = result.offset_samples;
+ out->confidence = result.confidence;
+ out->valid = result.valid ? 1 : 0;
+ return OAKENGINE_OK;
+}
+
+extern "C" int oakengine_audio_estimate_stretch_and_offset(
+ const double *reference, int reference_len,
+ const double *candidate, int candidate_len,
+ const bool *reference_valid, int reference_valid_len,
+ const bool *candidate_valid, int candidate_valid_len,
+ uint64_t window_samples, int64_t max_offset_windows,
+ double min_rate, double max_rate, double rate_step,
+ oak_audio_waveform_stretch_offset *out)
+{
+ if (!out || !reference || !candidate || reference_len < 0 ||
+ candidate_len < 0 || !window_samples) {
+ return OAKENGINE_E_INVALID;
+ }
+
+ if ((reference_valid && reference_valid_len != reference_len) ||
+ (candidate_valid && candidate_valid_len != candidate_len)) {
+ return OAKENGINE_E_INVALID;
+ }
+
+ QVector ref(reference_len);
+ std::copy(reference, reference + reference_len, ref.begin());
+ QVector cand(candidate_len);
+ std::copy(candidate, candidate + candidate_len, cand.begin());
+
+ QVector ref_valid;
+ if (reference_valid) {
+ ref_valid.resize(reference_valid_len);
+ std::copy(reference_valid, reference_valid + reference_valid_len,
+ ref_valid.begin());
+ }
+
+ QVector cand_valid;
+ if (candidate_valid) {
+ cand_valid.resize(candidate_valid_len);
+ std::copy(candidate_valid, candidate_valid + candidate_valid_len,
+ cand_valid.begin());
+ }
+
+ const olive::AudioWaveformSync::StretchOffsetResult result =
+ olive::AudioWaveformSync::estimate_stretch_and_offset(
+ ref, cand, ref_valid, cand_valid, window_samples, max_offset_windows,
+ min_rate, max_rate, rate_step);
+
+ out->rate = result.rate;
+ out->offset_samples = result.offset_samples;
+ out->confidence = result.confidence;
+ out->valid = result.valid ? 1 : 0;
+ return OAKENGINE_OK;
+}
+
+extern "C" int oakengine_audio_sync_place_by_source_time(
+ const oak_audio_sync_source_clip *reference,
+ const oak_audio_sync_source_clip *candidate,
+ int64_t reference_timeline_in_num, int64_t reference_timeline_in_den,
+ oak_audio_sync_placement *out)
+{
+ if (!reference || !candidate || !out) {
+ return OAKENGINE_E_INVALID;
+ }
+
+ olive::AudioSynchronizer::SourceClip ref;
+ ref.source_start_time = olive::core::Rational(
+ reference->source_start_time_num, reference->source_start_time_den);
+ ref.media_in =
+ olive::core::Rational(reference->media_in_num, reference->media_in_den);
+ ref.has_source_start_time = reference->has_source_start_time != 0;
+
+ olive::AudioSynchronizer::SourceClip cand;
+ cand.source_start_time = olive::core::Rational(
+ candidate->source_start_time_num, candidate->source_start_time_den);
+ cand.media_in =
+ olive::core::Rational(candidate->media_in_num, candidate->media_in_den);
+ cand.has_source_start_time = candidate->has_source_start_time != 0;
+
+ const olive::core::Rational timeline_in(reference_timeline_in_num,
+ reference_timeline_in_den);
+ const olive::AudioSynchronizer::Placement placement =
+ olive::AudioSynchronizer::place_by_source_time(ref, cand, timeline_in);
+
+ out->timeline_in_num = placement.timeline_in.numerator();
+ out->timeline_in_den = placement.timeline_in.denominator();
+ out->valid = placement.valid ? 1 : 0;
+ return OAKENGINE_OK;
+}
+
+extern "C" int oakengine_audio_sync_place_by_waveform_offset(
+ int64_t reference_timeline_in_num, int64_t reference_timeline_in_den,
+ int64_t candidate_offset_samples, int sample_rate,
+ oak_audio_sync_placement *out)
+{
+ if (!out || sample_rate <= 0) {
+ return OAKENGINE_E_INVALID;
+ }
+
+ const olive::core::Rational timeline_in(reference_timeline_in_num,
+ reference_timeline_in_den);
+ const olive::AudioSynchronizer::Placement placement =
+ olive::AudioSynchronizer::place_by_waveform_offset(
+ timeline_in, candidate_offset_samples, sample_rate);
+
+ out->timeline_in_num = placement.timeline_in.numerator();
+ out->timeline_in_den = placement.timeline_in.denominator();
+ out->valid = placement.valid ? 1 : 0;
+ return OAKENGINE_OK;
+}
+
+/* ---- Audio format processor (R6 P5) ------------------------------------- */
+
+namespace
+{
+
+olive::core::AudioParams params_from_c(const OakAudioParams *p)
+{
+ // AudioParams takes ownership of the handle, so hand it a copy.
+ return olive::core::AudioParams::from_handle(oakcore_audioparams_copy(p));
+}
+
+} // namespace
+
+struct OakEngineAudioProcessor {
+ olive::AudioProcessor proc;
+
+ // Holds the packed output of the most recent convert() so the caller can
+ // borrow the bytes across the C boundary.
+ olive::AudioProcessor::Buffer buf;
+};
+
+extern "C" OakEngineAudioProcessor *oakengine_audio_processor_create(void)
+{
+ return new (std::nothrow) OakEngineAudioProcessor();
+}
+
+extern "C" void oakengine_audio_processor_free(OakEngineAudioProcessor *p)
+{
+ delete p;
+}
+
+extern "C" int oakengine_audio_processor_open(OakEngineAudioProcessor *p,
+ const OakAudioParams *from,
+ const OakAudioParams *to,
+ double tempo)
+{
+ if (!p || !from || !to) {
+ return OAKENGINE_E_INVALID;
+ }
+
+ const olive::core::AudioParams cpp_from = params_from_c(from);
+ const olive::core::AudioParams cpp_to = params_from_c(to);
+ return p->proc.open(cpp_from, cpp_to, tempo) ? OAKENGINE_OK
+ : OAKENGINE_E_FAILED;
+}
+
+extern "C" void oakengine_audio_processor_close(OakEngineAudioProcessor *p)
+{
+ if (p) {
+ p->buf.clear();
+ p->proc.close();
+ }
+}
+
+extern "C" int oakengine_audio_processor_is_open(OakEngineAudioProcessor *p)
+{
+ return (p && p->proc.is_open()) ? 1 : 0;
+}
+
+extern "C" int oakengine_audio_processor_convert(OakEngineAudioProcessor *p,
+ float **in, int nb_in_samples,
+ const void **out_data,
+ int *out_size)
+{
+ if (!p) {
+ return OAKENGINE_E_INVALID;
+ }
+
+ if (out_data) {
+ *out_data = nullptr;
+ }
+ if (out_size) {
+ *out_size = 0;
+ }
+
+ p->buf.clear();
+ const int r = p->proc.convert(in, nb_in_samples, &p->buf);
+ if (r < 0) {
+ return r;
+ }
+
+ if (!p->buf.empty()) {
+ if (out_data) {
+ *out_data = p->buf.at(0).constData();
+ }
+ if (out_size) {
+ *out_size = p->buf.at(0).size();
+ }
+ }
+ return r;
+}
+
+extern "C" OakAudioParams *oakengine_audio_processor_output_params(
+ OakEngineAudioProcessor *p)
+{
+ if (!p || !p->proc.is_open()) {
+ return nullptr;
+ }
+ return oakcore_audioparams_copy(p->proc.to().handle());
+}
diff --git a/engine/src/capi/color.cpp b/engine/src/capi/color.cpp
new file mode 100644
index 000000000..9e1a6da5f
--- /dev/null
+++ b/engine/src/capi/color.cpp
@@ -0,0 +1,448 @@
+/***
+
+ 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 .
+
+***/
+
+#include "oakengine/color.h"
+
+#include
+
+#include
+
+#include "colorinternal.h"
+#include "node/color/colormanager/colormanager.h"
+#include "node/project.h"
+#include "render/job/colortransformjob.h"
+#include "render/previewautocacher.h"
+#include "render/rendermanager.h"
+
+// The OakEngineColorProcessor handle layout is shared with the other capi
+// translation units via colorinternal.h.
+struct OakEngineColorConfig {
+ ocio::ConstConfigRcPtr ptr;
+};
+
+namespace
+{
+
+// 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());
+}
+
+thread_local QString g_last_error;
+
+void set_error(const QString &error)
+{
+ g_last_error = error;
+}
+
+olive::ColorManager *impl(const OakEngineColorManager *h)
+{
+ return reinterpret_cast(
+ const_cast(h));
+}
+
+olive::ColorTransform to_cpp(const oak_color_transform &t)
+{
+ if (t.is_display) {
+ return olive::ColorTransform(
+ t.output ? QString::fromUtf8(t.output) : QString(),
+ t.view ? QString::fromUtf8(t.view) : QString(),
+ t.look ? QString::fromUtf8(t.look) : QString());
+ }
+ return olive::ColorTransform(t.output ? QString::fromUtf8(t.output) :
+ QString());
+}
+
+// The engine's list accessors dereference the config unconditionally;
+// guard here so a manager whose config failed to load yields empty lists
+// instead of crashing.
+bool has_config(const olive::ColorManager *mgr)
+{
+ return mgr && mgr->get_config();
+}
+
+QString list_at(const QStringList &l, int index)
+{
+ return (index >= 0 && index < l.size()) ? l.at(index) : QString();
+}
+
+} // namespace
+
+extern "C" {
+
+int oakengine_color_last_error(char *buf, int buf_size)
+{
+ return string_to_buf(g_last_error, buf, buf_size);
+}
+
+OakEngineColorManager *
+oakengine_color_manager_from_project(OakEngineProject *project)
+{
+ if (!project) {
+ return nullptr;
+ }
+ auto *p = reinterpret_cast(project);
+ return reinterpret_cast(p->color_manager());
+}
+
+int oakengine_color_manager_get_config_filename(
+ const OakEngineColorManager *mgr, char *buf, int buf_size)
+{
+ if (!mgr) {
+ return OAKENGINE_E_INVALID;
+ }
+ return string_to_buf(impl(mgr)->get_config_filename(), buf, buf_size);
+}
+
+int oakengine_color_manager_set_config_filename(OakEngineColorManager *mgr,
+ const char *filename)
+{
+ if (!mgr || !filename) {
+ return OAKENGINE_E_INVALID;
+ }
+ impl(mgr)->set_config_filename(QString::fromUtf8(filename));
+ return OAKENGINE_OK;
+}
+
+int oakengine_color_manager_colorspace_count(const OakEngineColorManager *mgr)
+{
+ if (!has_config(impl(mgr))) {
+ return 0;
+ }
+ return impl(mgr)->list_available_colorspaces().size();
+}
+
+int oakengine_color_manager_colorspace_at(const OakEngineColorManager *mgr,
+ int index, char *buf, int buf_size)
+{
+ if (!has_config(impl(mgr))) {
+ return OAKENGINE_E_INVALID;
+ }
+ const QString s =
+ list_at(impl(mgr)->list_available_colorspaces(), index);
+ if (s.isNull()) {
+ return OAKENGINE_E_INVALID;
+ }
+ return string_to_buf(s, buf, buf_size);
+}
+
+int oakengine_color_manager_display_count(const OakEngineColorManager *mgr)
+{
+ if (!has_config(impl(mgr))) {
+ return 0;
+ }
+ return impl(mgr)->list_available_displays().size();
+}
+
+int oakengine_color_manager_display_at(const OakEngineColorManager *mgr,
+ int index, char *buf, int buf_size)
+{
+ if (!has_config(impl(mgr))) {
+ return OAKENGINE_E_INVALID;
+ }
+ const QString s = list_at(impl(mgr)->list_available_displays(), index);
+ if (s.isNull()) {
+ return OAKENGINE_E_INVALID;
+ }
+ return string_to_buf(s, buf, buf_size);
+}
+
+int oakengine_color_manager_view_count(const OakEngineColorManager *mgr,
+ const char *display)
+{
+ if (!has_config(impl(mgr))) {
+ return 0;
+ }
+ return impl(mgr)
+ ->list_available_views(display ? QString::fromUtf8(display) : QString())
+ .size();
+}
+
+int oakengine_color_manager_view_at(const OakEngineColorManager *mgr,
+ const char *display, int index, char *buf,
+ int buf_size)
+{
+ if (!has_config(impl(mgr))) {
+ return OAKENGINE_E_INVALID;
+ }
+ const QString s = list_at(
+ impl(mgr)->list_available_views(display ? QString::fromUtf8(display) :
+ QString()),
+ index);
+ if (s.isNull()) {
+ return OAKENGINE_E_INVALID;
+ }
+ return string_to_buf(s, buf, buf_size);
+}
+
+int oakengine_color_manager_look_count(const OakEngineColorManager *mgr)
+{
+ if (!has_config(impl(mgr))) {
+ return 0;
+ }
+ return impl(mgr)->list_available_looks().size();
+}
+
+int oakengine_color_manager_look_at(const OakEngineColorManager *mgr,
+ int index, char *buf, int buf_size)
+{
+ if (!has_config(impl(mgr))) {
+ return OAKENGINE_E_INVALID;
+ }
+ const QString s = list_at(impl(mgr)->list_available_looks(), index);
+ if (s.isNull()) {
+ return OAKENGINE_E_INVALID;
+ }
+ return string_to_buf(s, buf, buf_size);
+}
+
+int oakengine_color_manager_default_display(const OakEngineColorManager *mgr,
+ char *buf, int buf_size)
+{
+ if (!has_config(impl(mgr))) {
+ return OAKENGINE_E_INVALID;
+ }
+ return string_to_buf(impl(mgr)->get_default_display(), buf, buf_size);
+}
+
+int oakengine_color_manager_default_view(const OakEngineColorManager *mgr,
+ const char *display, char *buf,
+ int buf_size)
+{
+ if (!has_config(impl(mgr))) {
+ return OAKENGINE_E_INVALID;
+ }
+ return string_to_buf(
+ impl(mgr)->get_default_view(display ? QString::fromUtf8(display) :
+ QString()),
+ buf, buf_size);
+}
+
+int oakengine_color_manager_default_input_color_space(
+ const OakEngineColorManager *mgr, char *buf, int buf_size)
+{
+ if (!mgr) {
+ return OAKENGINE_E_INVALID;
+ }
+ return string_to_buf(impl(mgr)->get_default_input_color_space(), buf,
+ buf_size);
+}
+
+int oakengine_color_manager_set_default_input_color_space(
+ OakEngineColorManager *mgr, const char *colorspace)
+{
+ if (!mgr || !colorspace) {
+ return OAKENGINE_E_INVALID;
+ }
+ impl(mgr)->set_default_input_color_space(QString::fromUtf8(colorspace));
+ return OAKENGINE_OK;
+}
+
+int oakengine_color_manager_reference_color_space(
+ const OakEngineColorManager *mgr, char *buf, int buf_size)
+{
+ if (!mgr) {
+ return OAKENGINE_E_INVALID;
+ }
+ return string_to_buf(impl(mgr)->get_reference_color_space(), buf,
+ buf_size);
+}
+
+int oakengine_color_manager_default_luma_coefs(
+ const OakEngineColorManager *mgr, double *rgb)
+{
+ if (!mgr || !rgb) {
+ return OAKENGINE_E_INVALID;
+ }
+ impl(mgr)->get_default_luma_coefs(rgb);
+ return OAKENGINE_OK;
+}
+
+int oakengine_color_manager_compliant_color_space(
+ const OakEngineColorManager *mgr, const char *name, char *buf,
+ int buf_size)
+{
+ if (!has_config(impl(mgr)) || !name) {
+ return OAKENGINE_E_INVALID;
+ }
+ return string_to_buf(
+ impl(mgr)->get_compliant_color_space(QString::fromUtf8(name)), buf,
+ buf_size);
+}
+
+int oakengine_color_manager_compliant_transform(
+ const OakEngineColorManager *mgr, const oak_color_transform *in,
+ int force_display, int *out_is_display, char *out_output,
+ int output_size, char *out_view, int view_size, char *out_look,
+ int look_size)
+{
+ if (!has_config(impl(mgr)) || !in) {
+ return OAKENGINE_E_INVALID;
+ }
+ const olive::ColorTransform compliant =
+ impl(mgr)->get_compliant_color_space(to_cpp(*in), force_display != 0);
+ if (out_is_display) {
+ *out_is_display = compliant.is_display() ? 1 : 0;
+ }
+ string_to_buf(compliant.output(), out_output, output_size);
+ string_to_buf(compliant.view(), out_view, view_size);
+ string_to_buf(compliant.look(), out_look, look_size);
+ return OAKENGINE_OK;
+}
+
+OakEngineColorConfig *oakengine_color_config_load_default(void)
+{
+ try {
+ ocio::ConstConfigRcPtr c = olive::ColorManager::get_default_config();
+ if (!c) {
+ set_error(QStringLiteral("no default OCIO config available"));
+ return nullptr;
+ }
+ set_error(QString());
+ return new OakEngineColorConfig{std::move(c)};
+ } catch (ocio::Exception &e) {
+ set_error(QString::fromUtf8(e.what()));
+ return nullptr;
+ }
+}
+
+OakEngineColorConfig *oakengine_color_config_load_file(const char *filename)
+{
+ if (!filename) {
+ set_error(QStringLiteral("no filename given"));
+ return nullptr;
+ }
+ try {
+ ocio::ConstConfigRcPtr c =
+ olive::ColorManager::create_config_from_file(
+ QString::fromUtf8(filename));
+ set_error(QString());
+ return new OakEngineColorConfig{std::move(c)};
+ } catch (ocio::Exception &e) {
+ set_error(QString::fromUtf8(e.what()));
+ return nullptr;
+ }
+}
+
+void oakengine_color_config_free(OakEngineColorConfig *config)
+{
+ delete config;
+}
+
+int oakengine_color_config_colorspace_count(const OakEngineColorConfig *config)
+{
+ if (!config || !config->ptr) {
+ return 0;
+ }
+ return olive::ColorManager::list_available_colorspaces(config->ptr).size();
+}
+
+int oakengine_color_config_colorspace_at(const OakEngineColorConfig *config,
+ int index, char *buf, int buf_size)
+{
+ if (!config || !config->ptr) {
+ return OAKENGINE_E_INVALID;
+ }
+ const QString s = list_at(
+ olive::ColorManager::list_available_colorspaces(config->ptr), index);
+ if (s.isNull()) {
+ return OAKENGINE_E_INVALID;
+ }
+ return string_to_buf(s, buf, buf_size);
+}
+
+OakEngineColorProcessor *oakengine_color_processor_create(
+ const OakEngineColorManager *mgr, const char *input,
+ const oak_color_transform *dest, int direction)
+{
+ if (!mgr || !input || !dest ||
+ (direction != OAKENGINE_COLOR_PROCESSOR_NORMAL &&
+ direction != OAKENGINE_COLOR_PROCESSOR_INVERSE)) {
+ return nullptr;
+ }
+ // ColorProcessor catches OCIO failures internally and leaves the
+ // processor null (see engine/render/colorprocessor.cpp), so this never
+ // throws; validity is reported through is_valid().
+ auto *proc = new OakEngineColorProcessor;
+ proc->ptr = olive::ColorProcessor::create(
+ impl(mgr), QString::fromUtf8(input), to_cpp(*dest),
+ direction == OAKENGINE_COLOR_PROCESSOR_INVERSE ?
+ olive::ColorProcessor::k_inverse :
+ olive::ColorProcessor::k_normal);
+ return proc;
+}
+
+void oakengine_color_processor_free(OakEngineColorProcessor *proc)
+{
+ delete proc;
+}
+
+int oakengine_color_processor_is_valid(const OakEngineColorProcessor *proc)
+{
+ return (proc && proc->ptr && proc->ptr->get_processor()) ? 1 : 0;
+}
+
+int oakengine_color_processor_convert_color(
+ const OakEngineColorProcessor *proc, const double *in_rgba,
+ double *out_rgba)
+{
+ if (!proc || !proc->ptr || !in_rgba || !out_rgba) {
+ return OAKENGINE_E_INVALID;
+ }
+ const olive::Color out = proc->ptr->convert_color(
+ olive::Color(in_rgba[0], in_rgba[1], in_rgba[2], in_rgba[3]));
+ out_rgba[0] = out.red();
+ out_rgba[1] = out.green();
+ out_rgba[2] = out.blue();
+ out_rgba[3] = out.alpha();
+ return OAKENGINE_OK;
+}
+
+int oakengine_color_processor_id(const OakEngineColorProcessor *proc,
+ char *buf, int buf_size)
+{
+ if (!proc || !proc->ptr) {
+ return OAKENGINE_E_INVALID;
+ }
+ const char *id = proc->ptr->id();
+ const int len = id ? int(strlen(id)) : 0;
+ if (buf && buf_size > 0) {
+ snprintf(buf, size_t(buf_size), "%s", id ? id : "");
+ }
+ return len;
+}
+
+int oakengine_color_transform_job_set_processor(
+ void *job, const OakEngineColorProcessor *proc)
+{
+ if (!job) {
+ return OAKENGINE_E_INVALID;
+ }
+ auto *j = reinterpret_cast(job);
+ j->set_color_processor(proc ? proc->ptr : olive::ColorProcessorPtr());
+ return OAKENGINE_OK;
+}
+
+} // extern "C"
diff --git a/engine/src/capi/colorinternal.h b/engine/src/capi/colorinternal.h
new file mode 100644
index 000000000..36b08f7e2
--- /dev/null
+++ b/engine/src/capi/colorinternal.h
@@ -0,0 +1,38 @@
+/***
+
+ 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 .
+
+***/
+
+#ifndef OAKENGINE_COLORINTERNAL_H
+#define OAKENGINE_COLORINTERNAL_H
+
+// Internal (not installed) shared definition of the opaque color-processor
+// handle between color.cpp and the other capi translation units. The
+// public header (oakengine/color.h) only forward-declares
+// OakEngineColorProcessor; capi code that needs to unwrap the handle (e.g.
+// renderer.cpp feeding the render cacher) includes this header.
+
+#include "render/colorprocessor.h"
+
+// Owned handle layout: the opaque C type is a heap box around the engine's
+// shared pointer (matching the refcounting the C++ API uses).
+struct OakEngineColorProcessor {
+ olive::ColorProcessorPtr ptr;
+};
+
+#endif // OAKENGINE_COLORINTERNAL_H
diff --git a/engine/src/capi/config.cpp b/engine/src/capi/config.cpp
new file mode 100644
index 000000000..f786e879d
--- /dev/null
+++ b/engine/src/capi/config.cpp
@@ -0,0 +1,130 @@
+/***
+
+ 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 .
+
+***/
+
+#include "oakengine/config.h"
+
+#include
+
+#include
+#include
+
+#include "config/config.h"
+
+namespace
+{
+
+int write_string(const QString &s, char *buf, int buf_size)
+{
+ const QByteArray utf8 = s.toUtf8();
+ const int len = int(utf8.size());
+ if (buf && buf_size > 0) {
+ const int n = qMin(len, buf_size - 1);
+ std::memcpy(buf, utf8.constData(), size_t(n));
+ buf[n] = '\0';
+ }
+ return len;
+}
+
+oakengine_config_error_fn g_error_fn = nullptr;
+void *g_error_userdata = nullptr;
+
+void error_handler(const QString &title, const QString &message)
+{
+ if (g_error_fn) {
+ const QByteArray t = title.toUtf8();
+ const QByteArray m = message.toUtf8();
+ g_error_fn(t.constData(), m.constData(), g_error_userdata);
+ }
+}
+
+} // namespace
+
+extern "C" int oakengine_config_load(void)
+{
+ olive::Config::load();
+ return OAKENGINE_OK;
+}
+
+extern "C" int oakengine_config_save(void)
+{
+ olive::Config::save();
+ return OAKENGINE_OK;
+}
+
+extern "C" int oakengine_config_get_string(const char *key, char *buf,
+ int buf_size)
+{
+ if (!key) {
+ return OAKENGINE_E_INVALID;
+ }
+ const QVariant v = olive::Config::current()[QString::fromUtf8(key)];
+ const QString s = v.toString();
+ return write_string(s, buf, buf_size);
+}
+
+extern "C" int oakengine_config_set_string(const char *key,
+ const char *value)
+{
+ if (!key) {
+ return OAKENGINE_E_INVALID;
+ }
+ olive::Config::current()[QString::fromUtf8(key)] =
+ QString::fromUtf8(value ? value : "");
+ return OAKENGINE_OK;
+}
+
+extern "C" int64_t oakengine_config_get_int(const char *key,
+ int64_t default_value)
+{
+ if (!key) {
+ return default_value;
+ }
+ const QVariant v = olive::Config::current()[QString::fromUtf8(key)];
+ bool ok = false;
+ const qlonglong val = v.toLongLong(&ok);
+ return ok ? static_cast(val) : default_value;
+}
+
+extern "C" int oakengine_config_set_int(const char *key, int64_t value)
+{
+ if (!key) {
+ return OAKENGINE_E_INVALID;
+ }
+ olive::Config::current()[QString::fromUtf8(key)] =
+ static_cast(value);
+ return OAKENGINE_OK;
+}
+
+extern "C" int oakengine_config_set_error_handler(
+ oakengine_config_error_fn fn, void *userdata)
+{
+ g_error_fn = fn;
+ g_error_userdata = userdata;
+ olive::Config::set_error_handler(fn ? error_handler : nullptr);
+ return OAKENGINE_OK;
+}
+
+extern "C" int oakengine_config_report_error(const char *title,
+ const char *message)
+{
+ olive::Config::report_error(QString::fromUtf8(title ? title : ""),
+ QString::fromUtf8(message ? message : ""));
+ return OAKENGINE_OK;
+}
diff --git a/engine/src/capi/disk.cpp b/engine/src/capi/disk.cpp
new file mode 100644
index 000000000..b51b4844e
--- /dev/null
+++ b/engine/src/capi/disk.cpp
@@ -0,0 +1,187 @@
+/***
+
+ 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 .
+
+***/
+
+#include "oakengine/disk.h"
+
+#include
+
+#include
+#include
+#include
+
+#include "node/project.h"
+#include "render/diskmanager.h"
+
+namespace
+{
+
+int write_string(const QString &s, char *buf, int buf_size)
+{
+ const QByteArray utf8 = s.toUtf8();
+ const int len = int(utf8.size());
+ if (buf && buf_size > 0) {
+ const int n = qMin(len, buf_size - 1);
+ std::memcpy(buf, utf8.constData(), size_t(n));
+ buf[n] = '\0';
+ }
+ return len;
+}
+
+olive::DiskManager *manager()
+{
+ return olive::DiskManager::instance();
+}
+
+olive::DiskCacheFolder *folder_from_path(olive::DiskManager *m,
+ const char *path)
+{
+ if (!m) {
+ return nullptr;
+ }
+ if (!path || std::strlen(path) == 0) {
+ return m->get_default_cache_folder();
+ }
+ return m->get_open_folder(QString::fromUtf8(path));
+}
+
+struct SettingsHandlerState {
+ oakengine_disk_settings_fn fn = nullptr;
+ void *userdata = nullptr;
+};
+
+SettingsHandlerState g_settings_handler;
+
+void cpp_settings_handler(olive::DiskCacheFolder *folder, QWidget *parent)
+{
+ if (!g_settings_handler.fn || !folder) {
+ return;
+ }
+ const QByteArray path = folder->get_path().toUtf8();
+ g_settings_handler.fn(path.constData(), parent, g_settings_handler.userdata);
+}
+
+} // namespace
+
+extern "C" int oakengine_disk_create_instance(void)
+{
+ olive::DiskManager::create_instance();
+ return manager() ? OAKENGINE_OK : OAKENGINE_E_FAILED;
+}
+
+extern "C" int oakengine_disk_destroy_instance(void)
+{
+ olive::DiskManager::destroy_instance();
+ return OAKENGINE_OK;
+}
+
+extern "C" int oakengine_disk_set_settings_handler(
+ oakengine_disk_settings_fn fn, void *userdata)
+{
+ g_settings_handler.fn = fn;
+ g_settings_handler.userdata = userdata;
+
+ olive::DiskManager::set_show_disk_cache_settings_handler(
+ fn ? cpp_settings_handler : olive::DiskManager::ShowDiskCacheSettingsHandler{});
+
+ return OAKENGINE_OK;
+}
+
+extern "C" int oakengine_disk_show_settings_dialog(const char *path,
+ void *parent_window)
+{
+ olive::DiskManager *m = manager();
+ if (!m) {
+ return OAKENGINE_E_STATE;
+ }
+
+ olive::DiskCacheFolder *folder = folder_from_path(m, path);
+ if (!folder) {
+ return OAKENGINE_E_FAILED;
+ }
+
+ m->show_disk_cache_settings_dialog(folder,
+ static_cast(parent_window));
+ return OAKENGINE_OK;
+}
+
+extern "C" int oakengine_disk_show_change_confirmation_dialog(
+ void *parent_window)
+{
+ return olive::DiskManager::show_disk_cache_change_confirmation_dialog(
+ static_cast(parent_window))
+ ? 1
+ : 0;
+}
+
+extern "C" int oakengine_disk_clear_cache(const char *path)
+{
+ olive::DiskManager *m = manager();
+ if (!m) {
+ return 0;
+ }
+
+ olive::DiskCacheFolder *folder = folder_from_path(m, path);
+ if (!folder) {
+ return 0;
+ }
+
+ return m->clear_disk_cache(folder->get_path()) ? 1 : 0;
+}
+
+extern "C" int oakengine_disk_get_default_cache_path(char *buf, int buf_size)
+{
+ olive::DiskManager *m = manager();
+ if (!m) {
+ return OAKENGINE_E_STATE;
+ }
+
+ return write_string(m->get_default_cache_path(), buf, buf_size);
+}
+
+extern "C" int oakengine_disk_set_default_cache_path(const char *path)
+{
+ if (!path) {
+ return OAKENGINE_E_INVALID;
+ }
+
+ olive::DiskManager *m = manager();
+ if (!m) {
+ return OAKENGINE_E_STATE;
+ }
+
+ m->get_default_cache_folder()->set_path(QString::fromUtf8(path));
+ return OAKENGINE_OK;
+}
+
+extern "C" void *oakengine_disk_get_open_folder(const char *path)
+{
+ return folder_from_path(manager(), path);
+}
+
+extern "C" int oakengine_disk_invalidate_project(OakEngineProject *project)
+{
+ olive::DiskManager *m = manager();
+ if (!m) {
+ return OAKENGINE_E_STATE;
+ }
+
+ emit m->invalidate_project(reinterpret_cast(project));
+ return OAKENGINE_OK;
+}
diff --git a/engine/src/capi/display.cpp b/engine/src/capi/display.cpp
new file mode 100644
index 000000000..369b72c3a
--- /dev/null
+++ b/engine/src/capi/display.cpp
@@ -0,0 +1,182 @@
+/***
+
+ 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 .
+
+***/
+
+#include "oakengine/display.h"
+
+#include
+#include
+#include
+
+#include "codec/frame.h"
+#include "render/job/colortransformjob.h"
+#include "render/opengl/openglrenderer.h"
+#include "render/renderer.h"
+#include "render/texture.h"
+#ifdef OAK_ENABLE_DYNAMIC_RENDER_BACKEND
+#include "render/backend/dynamicrenderer.h"
+#endif
+
+extern "C" {
+
+void *oakengine_display_renderer_create_dynamic(const char *backend_name,
+ void *parent)
+{
+#ifdef OAK_ENABLE_DYNAMIC_RENDER_BACKEND
+ QObject *p = static_cast(parent);
+ auto *dyn = new olive::DynamicRenderer(
+ QString::fromUtf8(backend_name ? backend_name : ""), p);
+ if (dyn->load()) {
+ return dyn;
+ }
+ // Backend library failed to load: drop it so the caller can fall back
+ // to the built-in OpenGL renderer.
+ delete dyn;
+ return nullptr;
+#else
+ (void)backend_name;
+ (void)parent;
+ return nullptr;
+#endif
+}
+
+void *oakengine_display_renderer_create_opengl(void *parent)
+{
+ return new olive::OpenGLRenderer(static_cast(parent));
+}
+
+int oakengine_display_renderer_init(void *renderer, void *gl_context)
+{
+ olive::Renderer *r = static_cast(renderer);
+ if (!r) {
+ return OAKENGINE_E_INVALID;
+ }
+
+ if (gl_context) {
+ QOpenGLContext *ctx = static_cast(gl_context);
+#ifdef OAK_ENABLE_DYNAMIC_RENDER_BACKEND
+ if (auto *dyn = dynamic_cast(r)) {
+ dyn->init_with_open_gl_context(ctx);
+ dyn->post_init();
+ return OAKENGINE_OK;
+ }
+#endif
+ auto *gl = static_cast(r);
+ gl->init(ctx);
+ gl->post_init();
+ return OAKENGINE_OK;
+ }
+
+ r->init();
+ r->post_init();
+ return OAKENGINE_OK;
+}
+
+void oakengine_display_renderer_destroy(void *renderer)
+{
+ olive::Renderer *r = static_cast(renderer);
+ if (!r) {
+ return;
+ }
+ r->destroy();
+ r->post_destroy();
+}
+
+void oakengine_display_renderer_create_texture(void *renderer,
+ const void *video_params,
+ const void *pixels, int linesize,
+ void *out_texture)
+{
+ olive::Renderer *r = static_cast(renderer);
+ if (!r || !video_params || !out_texture) {
+ return;
+ }
+ const olive::VideoParams ¶ms =
+ *static_cast(video_params);
+ *static_cast(out_texture) =
+ r->create_texture(params, pixels, linesize);
+}
+
+void oakengine_display_renderer_blit_color_managed(void *renderer,
+ const void *color_job,
+ void *dst_texture,
+ const void *video_params)
+{
+ olive::Renderer *r = static_cast(renderer);
+ if (!r || !color_job) {
+ return;
+ }
+ const olive::ColorTransformJob &job =
+ *static_cast(color_job);
+ olive::Texture *dst = static_cast(dst_texture);
+ if (video_params) {
+ r->blit_color_managed(
+ job, dst, *static_cast(video_params));
+ } else if (dst) {
+ r->blit_color_managed(job, dst, dst->params());
+ }
+}
+
+void oakengine_display_texture_upload(void *texture, void *pixels, int linesize)
+{
+ olive::Texture *t = static_cast(texture);
+ if (!t) {
+ return;
+ }
+ t->upload(pixels, linesize);
+}
+
+void oakengine_display_texture_download(void *texture, void *pixels,
+ int linesize)
+{
+ olive::Texture *t = static_cast(texture);
+ if (!t) {
+ return;
+ }
+ t->download(pixels, linesize);
+}
+
+void oakengine_codec_frame_create(void *out_frame)
+{
+ if (!out_frame) {
+ return;
+ }
+ *static_cast(out_frame) = olive::Frame::create();
+}
+
+void oakengine_codec_frame_set_video_params(void *frame,
+ const void *video_params)
+{
+ olive::Frame *f = static_cast(frame);
+ if (!f || !video_params) {
+ return;
+ }
+ f->set_video_params(*static_cast(video_params));
+}
+
+int oakengine_codec_frame_allocate(void *frame)
+{
+ olive::Frame *f = static_cast(frame);
+ if (!f) {
+ return 0;
+ }
+ return f->allocate() ? 1 : 0;
+}
+
+} // extern "C"
diff --git a/engine/src/capi/encoding.cpp b/engine/src/capi/encoding.cpp
new file mode 100644
index 000000000..dbc535f0a
--- /dev/null
+++ b/engine/src/capi/encoding.cpp
@@ -0,0 +1,1160 @@
+/***
+
+ 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 .
+
+***/
+
+#include "oakengine/encoding.h"
+#include "oakengine/exporter.h"
+
+#include
+#include
+#include
+
+#include "audio/audiomanager.h"
+#include "coreengine.h"
+#include "exportinternal.h"
+#include "node/project.h"
+#include "node/project/sequence/sequence.h"
+#include "render/rendermanager.h"
+#include "codec/encoder.h"
+#include "codec/ffmpeg/ffmpegencoder.h"
+#include "node/output/viewer/viewer.h"
+
+namespace
+{
+
+// 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());
+}
+
+bool valid_format(int format)
+{
+ return format >= 0 && format < olive::ExportFormat::k_format_count;
+}
+
+bool valid_codec(int codec)
+{
+ return codec >= 0 && codec < olive::ExportCodec::k_codec_count;
+}
+
+olive::VideoParams to_cpp(const oak_video_params &v)
+{
+ olive::VideoParams vp(
+ v.width, v.height,
+ olive::Rational(v.time_base_num, v.time_base_den),
+ static_cast(v.format),
+ olive::VideoParams::k_internal_channel_count,
+ olive::Rational(v.pixel_aspect_num, v.pixel_aspect_den),
+ static_cast(v.interlacing),
+ v.divider > 0 ? v.divider : 1);
+ vp.set_color_range(static_cast(v.color_range));
+ return vp;
+}
+
+void from_cpp(const olive::VideoParams &vp, oak_video_params *out)
+{
+ out->width = vp.width();
+ out->height = vp.height();
+ out->time_base_num = vp.time_base().numerator();
+ out->time_base_den = vp.time_base().denominator();
+ out->format = int(vp.format());
+ out->pixel_aspect_num = vp.pixel_aspect_ratio().numerator();
+ out->pixel_aspect_den = vp.pixel_aspect_ratio().denominator();
+ out->interlacing = int(vp.interlacing());
+ out->color_range = int(vp.color_range());
+ out->divider = vp.divider();
+}
+
+olive::EncodingParams *impl(OakEngineEncodingParams *p)
+{
+ return reinterpret_cast(p);
+}
+
+const olive::EncodingParams *impl(const OakEngineEncodingParams *p)
+{
+ return reinterpret_cast(p);
+}
+
+} // namespace
+
+struct OakEngineEncodingParams : public olive::EncodingParams {
+};
+
+extern "C"
+{
+
+/* ---- Container format / codec metadata ---------------------------------- */
+
+int oakengine_encoding_format_count(void)
+{
+ return olive::ExportFormat::k_format_count;
+}
+
+int oakengine_encoding_format_name(int format, char *buf, int buf_size)
+{
+ if (!valid_format(format)) {
+ return -1;
+ }
+ return string_to_buf(
+ olive::ExportFormat::get_name(olive::ExportFormat::Format(format)), buf,
+ buf_size);
+}
+
+int oakengine_encoding_format_extension(int format, char *buf, int buf_size)
+{
+ if (!valid_format(format)) {
+ return -1;
+ }
+ return string_to_buf(
+ olive::ExportFormat::get_extension(olive::ExportFormat::Format(format)),
+ buf, buf_size);
+}
+
+int oakengine_encoding_format_video_codec_count(int format)
+{
+ if (!valid_format(format)) {
+ return -1;
+ }
+ return olive::ExportFormat::get_video_codecs(
+ olive::ExportFormat::Format(format))
+ .size();
+}
+
+int oakengine_encoding_format_video_codec_at(int format, int index)
+{
+ if (!valid_format(format)) {
+ return -1;
+ }
+ const auto l =
+ olive::ExportFormat::get_video_codecs(olive::ExportFormat::Format(format));
+ return (index >= 0 && index < l.size()) ? int(l.at(index)) : -1;
+}
+
+int oakengine_encoding_format_audio_codec_count(int format)
+{
+ if (!valid_format(format)) {
+ return -1;
+ }
+ return olive::ExportFormat::get_audio_codecs(
+ olive::ExportFormat::Format(format))
+ .size();
+}
+
+int oakengine_encoding_format_audio_codec_at(int format, int index)
+{
+ if (!valid_format(format)) {
+ return -1;
+ }
+ const auto l =
+ olive::ExportFormat::get_audio_codecs(olive::ExportFormat::Format(format));
+ return (index >= 0 && index < l.size()) ? int(l.at(index)) : -1;
+}
+
+int oakengine_encoding_format_subtitle_codec_count(int format)
+{
+ if (!valid_format(format)) {
+ return -1;
+ }
+ return olive::ExportFormat::get_subtitle_codecs(
+ olive::ExportFormat::Format(format))
+ .size();
+}
+
+int oakengine_encoding_format_subtitle_codec_at(int format, int index)
+{
+ if (!valid_format(format)) {
+ return -1;
+ }
+ const auto l = olive::ExportFormat::get_subtitle_codecs(
+ olive::ExportFormat::Format(format));
+ return (index >= 0 && index < l.size()) ? int(l.at(index)) : -1;
+}
+
+int oakengine_encoding_codec_name(int codec, char *buf, int buf_size)
+{
+ if (!valid_codec(codec)) {
+ return -1;
+ }
+ return string_to_buf(
+ olive::ExportCodec::get_codec_name(olive::ExportCodec::Codec(codec)), buf,
+ buf_size);
+}
+
+int oakengine_encoding_codec_is_still_image(int codec)
+{
+ if (!valid_codec(codec)) {
+ return 0;
+ }
+ return olive::ExportCodec::is_codec_a_still_image(
+ olive::ExportCodec::Codec(codec)) ?
+ 1 :
+ 0;
+}
+
+int oakengine_encoding_codec_is_lossless(int codec)
+{
+ if (!valid_codec(codec)) {
+ return 0;
+ }
+ return olive::ExportCodec::is_codec_lossless(olive::ExportCodec::Codec(codec)) ?
+ 1 :
+ 0;
+}
+
+int oakengine_encoding_pix_fmt_count(int format, int codec)
+{
+ if (!valid_format(format) || !valid_codec(codec)) {
+ return -1;
+ }
+ return olive::ExportFormat::get_pixel_formats_for_codec(
+ olive::ExportFormat::Format(format),
+ olive::ExportCodec::Codec(codec))
+ .size();
+}
+
+int oakengine_encoding_pix_fmt_at(int format, int codec, int index, char *buf,
+ int buf_size)
+{
+ if (!valid_format(format) || !valid_codec(codec)) {
+ return -1;
+ }
+ const QStringList l = olive::ExportFormat::get_pixel_formats_for_codec(
+ olive::ExportFormat::Format(format), olive::ExportCodec::Codec(codec));
+ if (index < 0 || index >= l.size()) {
+ return -1;
+ }
+ return string_to_buf(l.at(index), buf, buf_size);
+}
+
+int oakengine_encoding_pix_fmt_index(int codec, const char *pix_fmt)
+{
+ if (!valid_codec(codec) || !pix_fmt || !pix_fmt[0]) {
+ return 0;
+ }
+ olive::FFmpegEncoder probe{ olive::EncodingParams() };
+ const int index =
+ probe.get_pixel_formats_for_codec(olive::ExportCodec::Codec(codec))
+ .indexOf(QString::fromUtf8(pix_fmt));
+ return index >= 0 ? index : 0;
+}
+
+int oakengine_encoding_sample_format_count(int format, int codec)
+{
+ if (!valid_format(format) || !valid_codec(codec)) {
+ return -1;
+ }
+ return int(olive::ExportFormat::get_sample_formats_for_codec(
+ olive::ExportFormat::Format(format),
+ olive::ExportCodec::Codec(codec))
+ .size());
+}
+
+int oakengine_encoding_sample_format_at(int format, int codec, int index)
+{
+ if (!valid_format(format) || !valid_codec(codec)) {
+ return -1;
+ }
+ const auto l = olive::ExportFormat::get_sample_formats_for_codec(
+ olive::ExportFormat::Format(format), olive::ExportCodec::Codec(codec));
+ return (index >= 0 && index < int(l.size())) ? int(l[size_t(index)]) : -1;
+}
+
+/* ---- Image-sequence filename helpers ------------------------------------ */
+
+int oakengine_encoding_filename_contains_digit_placeholder(const char *filename)
+{
+ if (!filename) {
+ return 0;
+ }
+ return olive::Encoder::filename_contains_digit_placeholder(
+ QString::fromUtf8(filename)) ?
+ 1 :
+ 0;
+}
+
+int oakengine_encoding_image_sequence_digit_count(const char *filename)
+{
+ if (!filename) {
+ return 0;
+ }
+ return olive::Encoder::get_image_sequence_placeholder_digit_count(
+ QString::fromUtf8(filename));
+}
+
+int oakengine_encoding_filename_remove_digit_placeholder(const char *filename,
+ char *buf, int buf_size)
+{
+ if (!filename) {
+ return -1;
+ }
+ return string_to_buf(olive::Encoder::filename_remove_digit_placeholder(
+ QString::fromUtf8(filename)),
+ buf, buf_size);
+}
+
+int oakengine_encoding_generate_matrix(int method, int src_width,
+ int src_height, int dest_width,
+ int dest_height, float out16[16])
+{
+ if (!out16 || method < 0 || method > 2 || src_width <= 0 || src_height <= 0 ||
+ dest_width <= 0 || dest_height <= 0) {
+ return OAKENGINE_E_INVALID;
+ }
+ const QMatrix4x4 m = olive::EncodingParams::generate_matrix(
+ olive::EncodingParams::VideoScalingMethod(method), src_width, src_height,
+ dest_width, dest_height);
+ m.copyDataTo(out16);
+ return OAKENGINE_OK;
+}
+
+/* ---- Encoding parameters handle ----------------------------------------- */
+
+OakEngineEncodingParams *oakengine_encoding_params_create(void)
+{
+ return new OakEngineEncodingParams;
+}
+
+void oakengine_encoding_params_destroy(OakEngineEncodingParams *params)
+{
+ delete params;
+}
+
+int oakengine_encoding_params_is_valid(const OakEngineEncodingParams *params)
+{
+ return params && impl(params)->is_valid() ? 1 : 0;
+}
+
+int oakengine_encoding_params_set_filename(OakEngineEncodingParams *params,
+ const char *filename)
+{
+ if (!params || !filename) {
+ return OAKENGINE_E_INVALID;
+ }
+ impl(params)->set_filename(QString::fromUtf8(filename));
+ return OAKENGINE_OK;
+}
+
+int oakengine_encoding_params_filename(const OakEngineEncodingParams *params,
+ char *buf, int buf_size)
+{
+ if (!params) {
+ return -1;
+ }
+ return string_to_buf(impl(params)->filename(), buf, buf_size);
+}
+
+int oakengine_encoding_params_set_format(OakEngineEncodingParams *params,
+ int format)
+{
+ if (!params || !valid_format(format)) {
+ return OAKENGINE_E_INVALID;
+ }
+ impl(params)->set_format(olive::ExportFormat::Format(format));
+ return OAKENGINE_OK;
+}
+
+int oakengine_encoding_params_format(const OakEngineEncodingParams *params)
+{
+ if (!params || impl(params)->format() == olive::ExportFormat::k_format_count) {
+ return -1;
+ }
+ return int(impl(params)->format());
+}
+
+int oakengine_encoding_params_enable_video(OakEngineEncodingParams *params,
+ const oak_video_params *video,
+ int codec)
+{
+ if (!params || !video || !valid_codec(codec) || video->width <= 0 ||
+ video->height <= 0 || video->time_base_num <= 0 ||
+ video->time_base_den <= 0) {
+ return OAKENGINE_E_INVALID;
+ }
+ impl(params)->enable_video(to_cpp(*video), olive::ExportCodec::Codec(codec));
+ return OAKENGINE_OK;
+}
+
+int oakengine_encoding_params_enable_audio(OakEngineEncodingParams *params,
+ int sample_rate,
+ uint64_t channel_layout,
+ int sample_format, int codec)
+{
+ if (!params || !valid_codec(codec) || sample_rate <= 0 ||
+ channel_layout == 0) {
+ return OAKENGINE_E_INVALID;
+ }
+ impl(params)->enable_audio(
+ olive::AudioParams(sample_rate, channel_layout,
+ olive::core::SampleFormat::Format(sample_format)),
+ olive::ExportCodec::Codec(codec));
+ return OAKENGINE_OK;
+}
+
+int oakengine_encoding_params_enable_subtitles(OakEngineEncodingParams *params,
+ int codec)
+{
+ if (!params || !valid_codec(codec)) {
+ return OAKENGINE_E_INVALID;
+ }
+ impl(params)->enable_subtitles(olive::ExportCodec::Codec(codec));
+ return OAKENGINE_OK;
+}
+
+int oakengine_encoding_params_enable_sidecar_subtitles(
+ OakEngineEncodingParams *params, int format, int codec)
+{
+ if (!params || !valid_format(format) || !valid_codec(codec)) {
+ return OAKENGINE_E_INVALID;
+ }
+ impl(params)->enable_sidecar_subtitles(olive::ExportFormat::Format(format),
+ olive::ExportCodec::Codec(codec));
+ return OAKENGINE_OK;
+}
+
+void oakengine_encoding_params_disable_video(OakEngineEncodingParams *params)
+{
+ if (params) {
+ impl(params)->disable_video();
+ }
+}
+
+void oakengine_encoding_params_disable_audio(OakEngineEncodingParams *params)
+{
+ if (params) {
+ impl(params)->disable_audio();
+ }
+}
+
+void oakengine_encoding_params_disable_subtitles(OakEngineEncodingParams *params)
+{
+ if (params) {
+ impl(params)->disable_subtitles();
+ }
+}
+
+int oakengine_encoding_params_video_enabled(const OakEngineEncodingParams *params)
+{
+ return params && impl(params)->video_enabled() ? 1 : 0;
+}
+
+int oakengine_encoding_params_video_codec(const OakEngineEncodingParams *params)
+{
+ return params ? int(impl(params)->video_codec()) : -1;
+}
+
+int oakengine_encoding_params_get_video_params(
+ const OakEngineEncodingParams *params, oak_video_params *out)
+{
+ if (!params || !out) {
+ return OAKENGINE_E_INVALID;
+ }
+ if (!impl(params)->video_enabled()) {
+ return OAKENGINE_E_STATE;
+ }
+ from_cpp(impl(params)->video_params(), out);
+ return OAKENGINE_OK;
+}
+
+int oakengine_encoding_params_audio_enabled(const OakEngineEncodingParams *params)
+{
+ return params && impl(params)->audio_enabled() ? 1 : 0;
+}
+
+int oakengine_encoding_params_audio_codec(const OakEngineEncodingParams *params)
+{
+ return params ? int(impl(params)->audio_codec()) : -1;
+}
+
+int oakengine_encoding_params_get_audio_params(
+ const OakEngineEncodingParams *params, int *sample_rate,
+ uint64_t *channel_layout, int *sample_format)
+{
+ if (!params) {
+ return OAKENGINE_E_INVALID;
+ }
+ if (!impl(params)->audio_enabled()) {
+ return OAKENGINE_E_STATE;
+ }
+ const olive::AudioParams &ap = impl(params)->audio_params();
+ if (sample_rate) {
+ *sample_rate = ap.sample_rate();
+ }
+ if (channel_layout) {
+ *channel_layout = ap.channel_layout();
+ }
+ if (sample_format) {
+ *sample_format = int(ap.format());
+ }
+ return OAKENGINE_OK;
+}
+
+int oakengine_encoding_params_subtitles_enabled(
+ const OakEngineEncodingParams *params)
+{
+ return params && impl(params)->subtitles_enabled() ? 1 : 0;
+}
+
+int oakengine_encoding_params_subtitles_are_sidecar(
+ const OakEngineEncodingParams *params)
+{
+ return params && impl(params)->subtitles_are_sidecar() ? 1 : 0;
+}
+
+int oakengine_encoding_params_subtitles_sidecar_format(
+ const OakEngineEncodingParams *params)
+{
+ return params ? int(impl(params)->subtitle_sidecar_fmt()) : -1;
+}
+
+int oakengine_encoding_params_subtitles_codec(
+ const OakEngineEncodingParams *params)
+{
+ return params ? int(impl(params)->subtitles_codec()) : -1;
+}
+
+void oakengine_encoding_params_set_video_bit_rate(
+ OakEngineEncodingParams *params, int64_t rate)
+{
+ if (params) {
+ impl(params)->set_video_bit_rate(rate);
+ }
+}
+
+int64_t
+oakengine_encoding_params_video_bit_rate(const OakEngineEncodingParams *params)
+{
+ return params ? impl(params)->video_bit_rate() : 0;
+}
+
+void oakengine_encoding_params_set_video_min_bit_rate(
+ OakEngineEncodingParams *params, int64_t rate)
+{
+ if (params) {
+ impl(params)->set_video_min_bit_rate(rate);
+ }
+}
+
+int64_t oakengine_encoding_params_video_min_bit_rate(
+ const OakEngineEncodingParams *params)
+{
+ return params ? impl(params)->video_min_bit_rate() : 0;
+}
+
+void oakengine_encoding_params_set_video_max_bit_rate(
+ OakEngineEncodingParams *params, int64_t rate)
+{
+ if (params) {
+ impl(params)->set_video_max_bit_rate(rate);
+ }
+}
+
+int64_t oakengine_encoding_params_video_max_bit_rate(
+ const OakEngineEncodingParams *params)
+{
+ return params ? impl(params)->video_max_bit_rate() : 0;
+}
+
+void oakengine_encoding_params_set_video_buffer_size(
+ OakEngineEncodingParams *params, int64_t size)
+{
+ if (params) {
+ impl(params)->set_video_buffer_size(size);
+ }
+}
+
+int64_t oakengine_encoding_params_video_buffer_size(
+ const OakEngineEncodingParams *params)
+{
+ return params ? impl(params)->video_buffer_size() : 0;
+}
+
+void oakengine_encoding_params_set_video_threads(OakEngineEncodingParams *params,
+ int threads)
+{
+ if (params) {
+ impl(params)->set_video_threads(threads);
+ }
+}
+
+int oakengine_encoding_params_video_threads(
+ const OakEngineEncodingParams *params)
+{
+ return params ? impl(params)->video_threads() : 0;
+}
+
+void oakengine_encoding_params_set_audio_bit_rate(
+ OakEngineEncodingParams *params, int64_t rate)
+{
+ if (params) {
+ impl(params)->set_audio_bit_rate(rate);
+ }
+}
+
+int64_t
+oakengine_encoding_params_audio_bit_rate(const OakEngineEncodingParams *params)
+{
+ return params ? impl(params)->audio_bit_rate() : 0;
+}
+
+int oakengine_encoding_params_set_video_pix_fmt(OakEngineEncodingParams *params,
+ const char *pix_fmt)
+{
+ if (!params || !pix_fmt) {
+ return OAKENGINE_E_INVALID;
+ }
+ impl(params)->set_video_pix_fmt(QString::fromUtf8(pix_fmt));
+ return OAKENGINE_OK;
+}
+
+int oakengine_encoding_params_video_pix_fmt(
+ const OakEngineEncodingParams *params, char *buf, int buf_size)
+{
+ if (!params) {
+ return -1;
+ }
+ return string_to_buf(impl(params)->video_pix_fmt(), buf, buf_size);
+}
+
+void oakengine_encoding_params_set_video_is_image_sequence(
+ OakEngineEncodingParams *params, int is_image_sequence)
+{
+ if (params) {
+ impl(params)->set_video_is_image_sequence(is_image_sequence != 0);
+ }
+}
+
+int oakengine_encoding_params_video_is_image_sequence(
+ const OakEngineEncodingParams *params)
+{
+ return params && impl(params)->video_is_image_sequence() ? 1 : 0;
+}
+
+int oakengine_encoding_params_set_color_transform(
+ OakEngineEncodingParams *params, const char *output_name)
+{
+ if (!params) {
+ return OAKENGINE_E_INVALID;
+ }
+ impl(params)->set_color_transform(
+ olive::ColorTransform(QString::fromUtf8(output_name ? output_name : "")));
+ return OAKENGINE_OK;
+}
+
+int oakengine_encoding_params_color_transform_output(
+ const OakEngineEncodingParams *params, char *buf, int buf_size)
+{
+ if (!params) {
+ return -1;
+ }
+ return string_to_buf(impl(params)->color_transform().output(), buf, buf_size);
+}
+
+void oakengine_encoding_params_set_export_length(
+ OakEngineEncodingParams *params, int num, int den)
+{
+ if (params && den != 0) {
+ impl(params)->set_export_length(olive::Rational(num, den));
+ }
+}
+
+int oakengine_encoding_params_get_export_length(
+ const OakEngineEncodingParams *params, int *num, int *den)
+{
+ if (!params) {
+ return OAKENGINE_E_INVALID;
+ }
+ const olive::Rational r = impl(params)->get_export_length();
+ if (num) {
+ *num = r.numerator();
+ }
+ if (den) {
+ *den = r.denominator();
+ }
+ return OAKENGINE_OK;
+}
+
+void oakengine_encoding_params_set_custom_range(OakEngineEncodingParams *params,
+ int64_t in_num, int64_t in_den,
+ int64_t out_num,
+ int64_t out_den)
+{
+ if (params && in_den != 0 && out_den != 0) {
+ impl(params)->set_custom_range(
+ olive::TimeRange(olive::Rational(in_num, in_den),
+ olive::Rational(out_num, out_den)));
+ }
+}
+
+int oakengine_encoding_params_has_custom_range(
+ const OakEngineEncodingParams *params)
+{
+ return params && impl(params)->has_custom_range() ? 1 : 0;
+}
+
+int oakengine_encoding_params_get_custom_range(
+ const OakEngineEncodingParams *params, int64_t *in_num, int64_t *in_den,
+ int64_t *out_num, int64_t *out_den)
+{
+ if (!params) {
+ return OAKENGINE_E_INVALID;
+ }
+ if (!impl(params)->has_custom_range()) {
+ return OAKENGINE_E_NOT_FOUND;
+ }
+ const olive::TimeRange &r = impl(params)->custom_range();
+ if (in_num) {
+ *in_num = r.in().numerator();
+ }
+ if (in_den) {
+ *in_den = r.in().denominator();
+ }
+ if (out_num) {
+ *out_num = r.out().numerator();
+ }
+ if (out_den) {
+ *out_den = r.out().denominator();
+ }
+ return OAKENGINE_OK;
+}
+
+int oakengine_encoding_params_set_video_scaling_method(
+ OakEngineEncodingParams *params, int method)
+{
+ if (!params || method < 0 || method > 2) {
+ return OAKENGINE_E_INVALID;
+ }
+ impl(params)->set_video_scaling_method(
+ olive::EncodingParams::VideoScalingMethod(method));
+ return OAKENGINE_OK;
+}
+
+int oakengine_encoding_params_video_scaling_method(
+ const OakEngineEncodingParams *params)
+{
+ return params ? int(impl(params)->video_scaling_method()) : -1;
+}
+
+int oakengine_encoding_params_set_video_option(OakEngineEncodingParams *params,
+ const char *key,
+ const char *value)
+{
+ if (!params || !key || !value) {
+ return OAKENGINE_E_INVALID;
+ }
+ impl(params)->set_video_option(QString::fromUtf8(key),
+ QString::fromUtf8(value));
+ return OAKENGINE_OK;
+}
+
+int oakengine_encoding_params_video_option(const OakEngineEncodingParams *params,
+ const char *key, char *buf,
+ int buf_size)
+{
+ if (!params || !key) {
+ return -1;
+ }
+ const QString k = QString::fromUtf8(key);
+ if (!impl(params)->has_video_opt(k)) {
+ return OAKENGINE_E_NOT_FOUND;
+ }
+ return string_to_buf(impl(params)->video_option(k), buf, buf_size);
+}
+
+/* ---- Presets ------------------------------------------------------------- */
+
+int oakengine_encoding_preset_path(char *buf, int buf_size)
+{
+ return string_to_buf(olive::EncodingParams::get_preset_path().absolutePath(),
+ buf, buf_size);
+}
+
+int oakengine_encoding_preset_count(void)
+{
+ return olive::EncodingParams::get_list_of_presets().size();
+}
+
+int oakengine_encoding_preset_name(int index, char *buf, int buf_size)
+{
+ const QStringList l = olive::EncodingParams::get_list_of_presets();
+ if (index < 0 || index >= l.size()) {
+ return -1;
+ }
+ return string_to_buf(l.at(index), buf, buf_size);
+}
+
+int oakengine_encoding_params_load_file(OakEngineEncodingParams *params,
+ const char *path)
+{
+ if (!params || !path) {
+ return OAKENGINE_E_INVALID;
+ }
+ QFile f(QString::fromUtf8(path));
+ if (!f.open(QFile::ReadOnly)) {
+ return OAKENGINE_E_FAILED;
+ }
+ const bool ok = impl(params)->load(&f);
+ f.close();
+ return ok ? OAKENGINE_OK : OAKENGINE_E_FAILED;
+}
+
+int oakengine_encoding_params_save_file(const OakEngineEncodingParams *params,
+ const char *path)
+{
+ if (!params || !path) {
+ return OAKENGINE_E_INVALID;
+ }
+ QFile f(QString::fromUtf8(path));
+ if (!f.open(QFile::WriteOnly)) {
+ return OAKENGINE_E_FAILED;
+ }
+ impl(params)->save(&f);
+ f.close();
+ return OAKENGINE_OK;
+}
+
+/* ---- Export execution / per-sequence last-used --------------------------- */
+
+OakEngineEncodingParams *
+oakengine_encoding_params_get_last_used(OakEngineSequence *seq)
+{
+ olive::ViewerOutput *viewer = reinterpret_cast(seq);
+ if (!viewer || !viewer->get_last_used_encoding_params().is_valid()) {
+ return nullptr;
+ }
+ auto *copy = new OakEngineEncodingParams;
+ *static_cast(copy) =
+ viewer->get_last_used_encoding_params();
+ return copy;
+}
+
+void oakengine_encoding_params_set_last_used(
+ OakEngineSequence *seq, const OakEngineEncodingParams *params)
+{
+ olive::ViewerOutput *viewer = reinterpret_cast(seq);
+ if (viewer && params) {
+ viewer->set_last_used_encoding_params(*impl(params));
+ }
+}
+
+int oakengine_encoding_start_audio_recording(
+ const OakEngineEncodingParams *params, char *errbuf, int errbuf_size)
+{
+ if (!params || !impl(params)->audio_enabled()) {
+ return OAKENGINE_E_INVALID;
+ }
+ if (!olive::AudioManager::instance()) {
+ return OAKENGINE_E_STATE;
+ }
+ QString error;
+ if (!olive::AudioManager::instance()->start_recording(*impl(params), &error)) {
+ string_to_buf(error, errbuf, errbuf_size);
+ return OAKENGINE_E_FAILED;
+ }
+ return OAKENGINE_OK;
+}
+
+/* ---- VideoParams static data (oakengine/videoparams.h) ------------------- */
+
+int oakengine_video_params_supported_frame_rate_count(void)
+{
+ return olive::VideoParams::k_supported_frame_rates.size();
+}
+
+int oakengine_video_params_supported_frame_rate_at(int index, int *num, int *den)
+{
+ const auto &l = olive::VideoParams::k_supported_frame_rates;
+ if (index < 0 || index >= l.size()) {
+ return OAKENGINE_E_INVALID;
+ }
+ if (num) {
+ *num = l.at(index).numerator();
+ }
+ if (den) {
+ *den = l.at(index).denominator();
+ }
+ return OAKENGINE_OK;
+}
+
+int oakengine_video_params_frame_rate_to_string(int num, int den, char *buf,
+ int buf_size)
+{
+ if (den == 0) {
+ return -1;
+ }
+ return string_to_buf(
+ olive::VideoParams::frame_rate_to_string(olive::Rational(num, den)), buf,
+ buf_size);
+}
+
+int oakengine_video_params_standard_pixel_aspect_count(void)
+{
+ return olive::VideoParams::k_standard_pixel_aspects.size();
+}
+
+int oakengine_video_params_standard_pixel_aspect_at(int index, int *num,
+ int *den)
+{
+ const auto &l = olive::VideoParams::k_standard_pixel_aspects;
+ if (index < 0 || index >= l.size()) {
+ return OAKENGINE_E_INVALID;
+ }
+ if (num) {
+ *num = l.at(index).numerator();
+ }
+ if (den) {
+ *den = l.at(index).denominator();
+ }
+ return OAKENGINE_OK;
+}
+
+int oakengine_video_params_standard_pixel_aspect_name(int index, char *buf,
+ int buf_size)
+{
+ const QStringList l =
+ olive::VideoParams::get_standard_pixel_aspect_ratio_names();
+ if (index < 0 || index >= l.size()) {
+ return -1;
+ }
+ return string_to_buf(l.at(index), buf, buf_size);
+}
+
+int oakengine_video_params_format_pixel_aspect_ratio_string(
+ const char *format, int num, int den, char *buf, int buf_size)
+{
+ if (!format || den == 0) {
+ return -1;
+ }
+ return string_to_buf(olive::VideoParams::format_pixel_aspect_ratio_string(
+ QString::fromUtf8(format), olive::Rational(num, den)),
+ buf, buf_size);
+}
+
+int oakengine_video_params_supported_divider_count(void)
+{
+ return olive::VideoParams::k_supported_dividers.size();
+}
+
+int oakengine_video_params_supported_divider_at(int index)
+{
+ const auto &l = olive::VideoParams::k_supported_dividers;
+ return (index >= 0 && index < l.size()) ? l.at(index) : -1;
+}
+
+int oakengine_video_params_divider_name(int divider, char *buf, int buf_size)
+{
+ return string_to_buf(olive::VideoParams::get_name_for_divider(divider), buf,
+ buf_size);
+}
+
+int oakengine_video_params_format_is_float(int format)
+{
+ return olive::VideoParams::format_is_float(olive::PixelFormat::Format(format)) ?
+ 1 :
+ 0;
+}
+
+int oakengine_video_params_pixel_format_name(int format, char *buf,
+ int buf_size)
+{
+ return string_to_buf(olive::VideoParams::get_format_name(
+ olive::PixelFormat::Format(format)),
+ buf, buf_size);
+}
+
+int oakengine_video_params_effective_size(int width, int height, int divider,
+ int *out_width, int *out_height)
+{
+ if (width <= 0 || height <= 0 || divider <= 0) {
+ return OAKENGINE_E_INVALID;
+ }
+ if (out_width) {
+ *out_width = olive::VideoParams::get_scaled_dimension(width, divider);
+ }
+ if (out_height) {
+ *out_height = olive::VideoParams::get_scaled_dimension(height, divider);
+ }
+ return OAKENGINE_OK;
+}
+
+int oakengine_video_params_make(oak_video_params *p, int width, int height,
+ int time_base_num, int time_base_den,
+ int format, int pixel_aspect_num,
+ int pixel_aspect_den, int interlacing,
+ int color_range, int divider)
+{
+ if (!p) {
+ return OAKENGINE_E_INVALID;
+ }
+ p->width = width;
+ p->height = height;
+ p->time_base_num = time_base_num;
+ p->time_base_den = time_base_den;
+ p->format = format;
+ p->pixel_aspect_num = pixel_aspect_num;
+ p->pixel_aspect_den = pixel_aspect_den;
+ p->interlacing = interlacing;
+ p->color_range = color_range;
+ p->divider = divider;
+ return OAKENGINE_OK;
+}
+
+void *oakengine_video_params_create(const oak_video_params *pod)
+{
+ if (!pod) {
+ return nullptr;
+ }
+
+ olive::VideoParams *p;
+ if (pod->width > 0 && pod->height > 0 && pod->time_base_num != 0 &&
+ pod->time_base_den != 0) {
+ p = new olive::VideoParams(
+ pod->width, pod->height,
+ olive::Rational(pod->time_base_num, pod->time_base_den),
+ olive::PixelFormat::Format(pod->format),
+ olive::VideoParams::k_internal_channel_count,
+ olive::Rational(pod->pixel_aspect_num, pod->pixel_aspect_den),
+ static_cast(pod->interlacing),
+ pod->divider > 0 ? pod->divider : 1);
+ } else if (pod->width > 0 && pod->height > 0) {
+ p = new olive::VideoParams(
+ pod->width, pod->height,
+ olive::PixelFormat::Format(pod->format),
+ olive::VideoParams::k_internal_channel_count,
+ olive::Rational(pod->pixel_aspect_num, pod->pixel_aspect_den),
+ static_cast(pod->interlacing),
+ pod->divider > 0 ? pod->divider : 1);
+ } else {
+ p = new olive::VideoParams();
+ }
+
+ p->set_color_range(
+ static_cast(pod->color_range));
+ p->set_video_type(static_cast(pod->video_type));
+ p->set_premultiplied_alpha(pod->premultiplied_alpha != 0);
+ return p;
+}
+
+void oakengine_video_params_free(void *params)
+{
+ delete static_cast(params);
+}
+
+int oakengine_video_params_equal(const oak_video_params *a,
+ const oak_video_params *b)
+{
+ if (!a || !b) {
+ return 0;
+ }
+ return (a->width == b->width && a->height == b->height &&
+ a->time_base_num == b->time_base_num &&
+ a->time_base_den == b->time_base_den && a->format == b->format &&
+ a->pixel_aspect_num == b->pixel_aspect_num &&
+ a->pixel_aspect_den == b->pixel_aspect_den &&
+ a->interlacing == b->interlacing && a->divider == b->divider) ?
+ 1 :
+ 0;
+}
+
+int oakengine_video_params_is_valid(const oak_video_params *p)
+{
+ if (!p) {
+ return 0;
+ }
+ const olive::VideoParams vp(
+ p->width, p->height,
+ olive::Rational(p->time_base_num, p->time_base_den),
+ olive::PixelFormat::Format(p->format),
+ olive::VideoParams::k_internal_channel_count,
+ olive::Rational(p->pixel_aspect_num, p->pixel_aspect_den),
+ olive::VideoParams::Interlacing(p->interlacing), p->divider);
+ return vp.is_valid() ? 1 : 0;
+}
+
+int oakengine_video_params_bytes_per_pixel(int format, int channels)
+{
+ return olive::VideoParams::get_bytes_per_pixel(
+ olive::PixelFormat::Format(format), channels);
+}
+
+int oakengine_video_params_internal_channel_count(void)
+{
+ return olive::VideoParams::k_internal_channel_count;
+}
+
+int oakengine_export_render_with_params(OakEngineSequence *seq,
+ const OakEngineEncodingParams *params)
+{
+ oakengine_export_set_error_string(QString());
+ if (!seq || !params) {
+ oakengine_export_set_error_string(
+ QStringLiteral("invalid arguments"));
+ return OAKENGINE_E_INVALID;
+ }
+ // Validate the sequence handle by pointer membership in the active
+ // project's node list. A dynamic_cast on a bogus handle (e.g. an
+ // OakEngineEncodingParams pointer, which has no vtable) crashes, and
+ // there is no safe way to dynamic_cast an arbitrary address -- pointer
+ // comparison is the only safe check. Limitation: the sequence must
+ // belong to the active project (same scope the export dialog uses).
+ olive::Sequence *sequence = nullptr;
+ if (olive::EngineCore::instance() &&
+ olive::EngineCore::instance()->open_project()) {
+ for (olive::Node *n :
+ olive::EngineCore::instance()->open_project()->nodes()) {
+ if (reinterpret_cast(n) == seq) {
+ sequence = dynamic_cast(n);
+ break;
+ }
+ }
+ }
+ if (!sequence) {
+ oakengine_export_set_error_string(
+ QStringLiteral("handle is not a sequence of the active project"));
+ return OAKENGINE_E_INVALID;
+ }
+ if (!olive::RenderManager::instance()) {
+ oakengine_export_set_error_string(
+ QStringLiteral("engine not initialized with "
+ "OAKENGINE_INIT_RENDER"));
+ return OAKENGINE_E_STATE;
+ }
+ olive::Project *project = sequence->project();
+ if (!project) {
+ oakengine_export_set_error_string(
+ QStringLiteral("sequence is not attached to a project"));
+ return OAKENGINE_E_INVALID;
+ }
+
+ // The handle publicly inherits olive::EncodingParams, so it drives the
+ // same synchronous ExportTask machinery as oakengine_export_render()/_ex()
+ // directly (progress callback + cancellation are shared engine state).
+ auto *ep = const_cast(params);
+ const int rc = oakengine_export_render_internal(
+ sequence, project, *ep, ep->audio_enabled(),
+ ep->audio_enabled() ? ep->audio_params()
+ : sequence->get_audio_params());
+ return rc;
+}
+
+} // extern "C"
diff --git a/engine/src/capi/events.cpp b/engine/src/capi/events.cpp
new file mode 100644
index 000000000..73eb47861
--- /dev/null
+++ b/engine/src/capi/events.cpp
@@ -0,0 +1,1221 @@
+/***
+
+ 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 .
+
+***/
+
+#include "oakengine/events.h"
+
+#include
+
+#include
+
+#include
+#include
+#include
+#include
+
+#include "audio/audiomanager.h"
+#include "coreengine.h"
+#include "node/keyframe.h"
+#include "oakengine/node.h"
+#include "node/block/block.h"
+#include "node/color/colormanager/colormanager.h"
+#include "node/group/group.h"
+#include "node/output/track/track.h"
+#include "node/output/track/tracklist.h"
+#include "node/output/viewer/viewer.h"
+#include "node/project.h"
+#include "node/project/folder/folder.h"
+#include "node/project/sequence/sequence.h"
+#include "render/framehashcache.h"
+#include "render/playbackcache.h"
+#include "task/task.h"
+#include "task/taskmanager.h"
+#include "timeline/timelinemarker.h"
+#include "timeline/timelineworkarea.h"
+#include "undo/undostack.h"
+
+namespace
+{
+
+// Subscription registry: id -> connections. Callbacks capture the function
+// pointer and userdata directly, so delivery never touches the registry;
+// the map only tracks lifecycle (unsubscribe, sender teardown).
+struct Subscription {
+ QVector connections;
+};
+
+QMutex g_registry_mutex;
+QHash g_registry;
+std::atomic g_next_id{1};
+
+// The observed engine object died: drop the registry entry. Qt has already
+// torn down the connections themselves.
+void drop_subscription(int64_t id)
+{
+ QMutexLocker locker(&g_registry_mutex);
+ g_registry.remove(id);
+}
+
+void invoke(oakengine_event_fn fn, void *userdata, int32_t id, void *source,
+ int64_t a, int64_t b, void *related, int64_t c = 0,
+ const char *s = nullptr)
+{
+ oakengine_event event;
+ event.id = id;
+ event.reserved = 0;
+ event.a = a;
+ event.b = b;
+ event.c = c;
+ event.source = source;
+ event.handle = related;
+ event.s = s;
+ fn(&event, userdata);
+}
+
+// Frame-timestamp timebase for node events: the frame rate of the
+// project's first sequence, or the engine default (1001/30000 s per
+// frame). Same convention as node.cpp's project_time_base().
+olive::Rational node_frame_time_base(const olive::Node *node)
+{
+ if (const olive::Project *p =
+ olive::Project::get_project_from_object(node)) {
+ for (olive::Node *n : p->nodes()) {
+ if (const olive::Sequence *s =
+ dynamic_cast(n)) {
+ const olive::Rational fr = s->get_video_params().frame_rate();
+ if (!fr.isNull() && !fr.isNaN()) {
+ return fr.flipped();
+ }
+ }
+ }
+ }
+ return olive::Rational(1001, 30000);
+}
+
+// NodeValue::Type -> facade value type (same mapping as node.cpp).
+int node_value_type_to_c(olive::NodeValue::Type t)
+{
+ switch (t) {
+ case olive::NodeValue::k_int:
+ return OAK_NODE_VALUE_INT;
+ case olive::NodeValue::k_float:
+ return OAK_NODE_VALUE_FLOAT;
+ case olive::NodeValue::k_boolean:
+ return OAK_NODE_VALUE_BOOL;
+ case olive::NodeValue::k_rational:
+ return OAK_NODE_VALUE_RATIONAL;
+ case olive::NodeValue::k_color:
+ return OAK_NODE_VALUE_COLOR;
+ case olive::NodeValue::k_vec2:
+ return OAK_NODE_VALUE_VEC2;
+ case olive::NodeValue::k_vec3:
+ return OAK_NODE_VALUE_VEC3;
+ case olive::NodeValue::k_vec4:
+ return OAK_NODE_VALUE_VEC4;
+ case olive::NodeValue::k_combo:
+ return OAK_NODE_VALUE_COMBO;
+ case olive::NodeValue::k_file:
+ return OAK_NODE_VALUE_STRING;
+ case olive::NodeValue::k_text:
+ return OAK_NODE_VALUE_TEXT;
+ case olive::NodeValue::k_font:
+ return OAK_NODE_VALUE_FONT;
+ case olive::NodeValue::k_str_combo:
+ return OAK_NODE_VALUE_STR_COMBO;
+ case olive::NodeValue::k_binary:
+ return OAK_NODE_VALUE_BINARY;
+ case olive::NodeValue::k_bezier:
+ return OAK_NODE_VALUE_BEZIER;
+ default:
+ return OAK_NODE_VALUE_NONE;
+}
+}
+
+// Wire the node-family events (handle validated as a Node). Appended to
+// `conns`; returns false when nothing matched.
+bool connect_node_event(olive::Node *node, int32_t event_id,
+ oakengine_event_fn fn, void *userdata,
+ QVector *conns)
+{
+ using namespace olive;
+
+ switch (event_id) {
+ case OAKENGINE_EVENT_NODE_LABEL_CHANGED:
+ conns->append(QObject::connect(
+ node, &Node::label_changed, node,
+ [fn, userdata, node](const QString &label) {
+ const QByteArray utf = label.toUtf8();
+ invoke(fn, userdata, OAKENGINE_EVENT_NODE_LABEL_CHANGED, node,
+ 0, 0, nullptr, 0, utf.constData());
+ },
+ Qt::DirectConnection));
+ return true;
+ case OAKENGINE_EVENT_NODE_INPUT_VALUE_CHANGED:
+ conns->append(QObject::connect(
+ node, &Node::value_changed, node,
+ [fn, userdata, node](const NodeInput &input,
+ const TimeRange &range) {
+ const Rational tb = node_frame_time_base(node);
+ const QByteArray utf = input.input().toUtf8();
+ invoke(fn, userdata,
+ OAKENGINE_EVENT_NODE_INPUT_VALUE_CHANGED, node,
+ input.element(),
+ core::Timecode::time_to_timestamp(
+ range.in(), tb, core::Timecode::k_round),
+ nullptr,
+ core::Timecode::time_to_timestamp(
+ range.out(), tb, core::Timecode::k_round),
+ utf.constData());
+ },
+ Qt::DirectConnection));
+ return true;
+ case OAKENGINE_EVENT_NODE_INPUT_CONNECTED:
+ case OAKENGINE_EVENT_NODE_INPUT_DISCONNECTED: {
+ const bool connected =
+ event_id == OAKENGINE_EVENT_NODE_INPUT_CONNECTED;
+ auto deliver = [fn, userdata, node, connected, event_id](
+ Node *output, const NodeInput &input) {
+ const QByteArray utf = input.input().toUtf8();
+ invoke(fn, userdata, event_id, node, input.element(), 0, output, 0,
+ utf.constData());
+ };
+ if (connected) {
+ conns->append(QObject::connect(node, &Node::input_connected, node,
+ deliver, Qt::DirectConnection));
+ } else {
+ conns->append(QObject::connect(node, &Node::input_disconnected,
+ node, deliver,
+ Qt::DirectConnection));
+ }
+ return true;
+ }
+ case OAKENGINE_EVENT_NODE_INPUT_FLAGS_CHANGED:
+ conns->append(QObject::connect(
+ node, &Node::input_flags_changed, node,
+ [fn, userdata, node](const QString &input,
+ const InputFlags &flags) {
+ const QByteArray utf = input.toUtf8();
+ invoke(fn, userdata, OAKENGINE_EVENT_NODE_INPUT_FLAGS_CHANGED,
+ node, int64_t(flags.value()), 0, nullptr, 0,
+ utf.constData());
+ },
+ Qt::DirectConnection));
+ return true;
+ case OAKENGINE_EVENT_NODE_INPUT_PROPERTY_CHANGED:
+ conns->append(QObject::connect(
+ node, &Node::input_property_changed, node,
+ [fn, userdata, node](const QString &input, const QString &,
+ const QVariant &) {
+ const QByteArray utf = input.toUtf8();
+ invoke(fn, userdata,
+ OAKENGINE_EVENT_NODE_INPUT_PROPERTY_CHANGED, node, 0, 0,
+ nullptr, 0, utf.constData());
+ },
+ Qt::DirectConnection));
+ return true;
+ case OAKENGINE_EVENT_NODE_INPUT_DATA_TYPE_CHANGED:
+ conns->append(QObject::connect(
+ node, &Node::input_data_type_changed, node,
+ [fn, userdata, node](const QString &input, NodeValue::Type type) {
+ const QByteArray utf = input.toUtf8();
+ invoke(fn, userdata,
+ OAKENGINE_EVENT_NODE_INPUT_DATA_TYPE_CHANGED, node,
+ node_value_type_to_c(type), 0, nullptr, 0,
+ utf.constData());
+ },
+ Qt::DirectConnection));
+ return true;
+ case OAKENGINE_EVENT_NODE_INPUT_ARRAY_SIZE_CHANGED:
+ conns->append(QObject::connect(
+ node, &Node::input_array_size_changed, node,
+ [fn, userdata, node](const QString &input, int old_size,
+ int new_size) {
+ const QByteArray utf = input.toUtf8();
+ invoke(fn, userdata,
+ OAKENGINE_EVENT_NODE_INPUT_ARRAY_SIZE_CHANGED, node,
+ old_size, new_size, nullptr, 0, utf.constData());
+ },
+ Qt::DirectConnection));
+ return true;
+ case OAKENGINE_EVENT_NODE_KEYFRAME_ENABLE_CHANGED:
+ conns->append(QObject::connect(
+ node, &Node::keyframe_enable_changed, node,
+ [fn, userdata, node](const NodeInput &input, bool enabled) {
+ const QByteArray utf = input.input().toUtf8();
+ invoke(fn, userdata,
+ OAKENGINE_EVENT_NODE_KEYFRAME_ENABLE_CHANGED, node,
+ input.element(), enabled ? 1 : 0, nullptr, 0,
+ utf.constData());
+ },
+ Qt::DirectConnection));
+ return true;
+ case OAKENGINE_EVENT_NODE_KEYFRAME_ADDED:
+ case OAKENGINE_EVENT_NODE_KEYFRAME_REMOVED: {
+ auto deliver = [fn, userdata, node, event_id](OakEngineKeyframe *k) {
+ auto *key = reinterpret_cast(k);
+ const QByteArray utf = key->input().toUtf8();
+ invoke(fn, userdata, event_id, node, key->element(), key->track(),
+ k, 0, utf.constData());
+ };
+ if (event_id == OAKENGINE_EVENT_NODE_KEYFRAME_ADDED) {
+ conns->append(QObject::connect(node, &Node::keyframe_added, node,
+ deliver, Qt::DirectConnection));
+ } else {
+ conns->append(QObject::connect(node, &Node::keyframe_removed,
+ node, deliver,
+ Qt::DirectConnection));
+ }
+ return true;
+ }
+ case OAKENGINE_EVENT_NODE_KEYFRAME_TIME_CHANGED:
+ case OAKENGINE_EVENT_NODE_KEYFRAME_TYPE_CHANGED:
+ case OAKENGINE_EVENT_NODE_KEYFRAME_VALUE_CHANGED: {
+ auto deliver = [fn, userdata, node, event_id](OakEngineKeyframe *k) {
+ invoke(fn, userdata, event_id, node, 0, 0, k);
+ };
+ if (event_id == OAKENGINE_EVENT_NODE_KEYFRAME_TIME_CHANGED) {
+ conns->append(QObject::connect(node, &Node::keyframe_time_changed,
+ node, deliver,
+ Qt::DirectConnection));
+ } else if (event_id == OAKENGINE_EVENT_NODE_KEYFRAME_TYPE_CHANGED) {
+ conns->append(QObject::connect(node, &Node::keyframe_type_changed,
+ node, deliver,
+ Qt::DirectConnection));
+ } else {
+ conns->append(QObject::connect(node,
+ &Node::keyframe_value_changed,
+ node, deliver,
+ Qt::DirectConnection));
+ }
+ return true;
+ }
+ case OAKENGINE_EVENT_NODE_NODE_ADDED_TO_CONTEXT:
+ case OAKENGINE_EVENT_NODE_NODE_REMOVED_FROM_CONTEXT: {
+ auto deliver = [fn, userdata, node, event_id](Node *child) {
+ invoke(fn, userdata, event_id, node, 0, 0, child);
+ };
+ if (event_id == OAKENGINE_EVENT_NODE_NODE_ADDED_TO_CONTEXT) {
+ conns->append(QObject::connect(node, &Node::node_added_to_context,
+ node, deliver,
+ Qt::DirectConnection));
+ } else {
+ conns->append(QObject::connect(node,
+ &Node::node_removed_from_context,
+ node, deliver,
+ Qt::DirectConnection));
+ }
+ return true;
+ }
+ case OAKENGINE_EVENT_NODE_MESSAGE_COUNT_CHANGED:
+ conns->append(QObject::connect(
+ node, &Node::message_count_changed, node,
+ [fn, userdata, node]() {
+ invoke(fn, userdata, OAKENGINE_EVENT_NODE_MESSAGE_COUNT_CHANGED,
+ node, 0, 0, nullptr);
+ },
+ Qt::DirectConnection));
+ return true;
+ case OAKENGINE_EVENT_GROUP_INPUT_PASSTHROUGH_ADDED:
+ case OAKENGINE_EVENT_GROUP_INPUT_PASSTHROUGH_REMOVED: {
+ auto *group = dynamic_cast(node);
+ if (!group) {
+ return false;
+ }
+ auto deliver = [fn, userdata, node, event_id](NodeGroup *,
+ const NodeInput &input) {
+ const QByteArray id = input.input().toUtf8();
+ invoke(fn, userdata, event_id, node, input.element(), 0,
+ input.node(), 0, id.constData());
+ };
+ if (event_id == OAKENGINE_EVENT_GROUP_INPUT_PASSTHROUGH_ADDED) {
+ conns->append(QObject::connect(
+ group, &NodeGroup::input_passthrough_added, group, deliver,
+ Qt::DirectConnection));
+ } else {
+ conns->append(QObject::connect(
+ group, &NodeGroup::input_passthrough_removed, group, deliver,
+ Qt::DirectConnection));
+ }
+ return true;
+ }
+ case OAKENGINE_EVENT_GROUP_OUTPUT_PASSTHROUGH_CHANGED: {
+ auto *group = dynamic_cast(node);
+ if (!group) {
+ return false;
+ }
+ conns->append(QObject::connect(
+ group, &NodeGroup::output_passthrough_changed, group,
+ [fn, userdata, node](NodeGroup *, Node *output) {
+ invoke(fn, userdata,
+ OAKENGINE_EVENT_GROUP_OUTPUT_PASSTHROUGH_CHANGED, node,
+ 0, 0, output);
+ },
+ Qt::DirectConnection));
+ return true;
+ }
+ case OAKENGINE_EVENT_NODE_CONTEXT_POSITION_CHANGED:
+ conns->append(QObject::connect(
+ node, &Node::node_position_in_context_changed, node,
+ [fn, userdata, node](Node *child, const QPointF &pos) {
+ int64_t xb, yb;
+ const double x = pos.x(), y = pos.y();
+ memcpy(&xb, &x, sizeof(xb));
+ memcpy(&yb, &y, sizeof(yb));
+ invoke(fn, userdata,
+ OAKENGINE_EVENT_NODE_CONTEXT_POSITION_CHANGED, node, xb,
+ yb, child);
+ },
+ Qt::DirectConnection));
+ return true;
+ case OAKENGINE_EVENT_NODE_LINKS_CHANGED:
+ conns->append(QObject::connect(
+ node, &Node::links_changed, node,
+ [fn, userdata, node]() {
+ invoke(fn, userdata, OAKENGINE_EVENT_NODE_LINKS_CHANGED, node,
+ 0, 0, nullptr);
+ },
+ Qt::DirectConnection));
+ return true;
+ case OAKENGINE_EVENT_NODE_COLOR_CHANGED:
+ conns->append(QObject::connect(
+ node, &Node::color_changed, node,
+ [fn, userdata, node]() {
+ invoke(fn, userdata, OAKENGINE_EVENT_NODE_COLOR_CHANGED, node,
+ 0, 0, nullptr);
+ },
+ Qt::DirectConnection));
+ return true;
+ case OAKENGINE_EVENT_NODE_INPUT_ADDED:
+ conns->append(QObject::connect(
+ node, &Node::input_added, node,
+ [fn, userdata, node](const QString &id) {
+ QByteArray utf = id.toUtf8();
+ invoke(fn, userdata, OAKENGINE_EVENT_NODE_INPUT_ADDED, node,
+ 0, 0, nullptr, 0, utf.constData());
+ },
+ Qt::DirectConnection));
+ return true;
+ case OAKENGINE_EVENT_NODE_INPUT_REMOVED:
+ conns->append(QObject::connect(
+ node, &Node::input_removed, node,
+ [fn, userdata, node](const QString &id) {
+ QByteArray utf = id.toUtf8();
+ invoke(fn, userdata, OAKENGINE_EVENT_NODE_INPUT_REMOVED, node,
+ 0, 0, nullptr, 0, utf.constData());
+ },
+ Qt::DirectConnection));
+ return true;
+ case OAKENGINE_EVENT_NODE_REMOVED_FROM_GRAPH:
+ conns->append(QObject::connect(
+ node, &Node::removed_from_graph, node,
+ [fn, userdata, node](olive::Project *project) {
+ invoke(fn, userdata, OAKENGINE_EVENT_NODE_REMOVED_FROM_GRAPH,
+ node,
+ 0, 0, reinterpret_cast(project));
+ },
+ Qt::DirectConnection));
+ return true;
+ default:
+ return false;
+ }
+}
+
+// The sequence's frame duration as a Rational timebase, like timeline.cpp.
+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;
+}
+
+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);
+}
+
+// Block range as frame timestamps in the track's sequence timebase; -1/-1
+// when the block is not on a sequenced track at emission time.
+void block_timestamps(const olive::Block *block, int64_t *in_ts,
+ int64_t *out_ts)
+{
+ *in_ts = -1;
+ *out_ts = -1;
+ if (!block || !block->track() || !block->track()->sequence()) {
+ return;
+ }
+ olive::Rational tb;
+ if (!time_base_of(block->track()->sequence(), &tb)) {
+ return;
+ }
+ *in_ts = time_to_ts(block->in(), tb);
+ *out_ts = time_to_ts(block->out(), tb);
+}
+
+int64_t marker_timestamp(const olive::Sequence *seq,
+ const olive::TimelineMarker *marker)
+{
+ olive::Rational tb;
+ if (!marker || !time_base_of(seq, &tb)) {
+ return -1;
+ }
+ return time_to_ts(marker->time().in(), tb);
+}
+
+// Wire the connections for one subscription. `obj` is the validated engine
+// object (already cast-checked). Returns the connection list, empty when
+// the event family does not match `obj`.
+QVector connect_event(
+ QObject *obj, int32_t event_id, oakengine_event_fn fn, void *userdata)
+{
+ using namespace olive;
+
+ QVector conns;
+
+ switch (event_id) {
+ case OAKENGINE_EVENT_PROJECT_MODIFIED_CHANGED: {
+ auto *project = dynamic_cast(obj);
+ if (!project) {
+ break;
+ }
+ conns.append(QObject::connect(
+ project, &Project::modified_changed, project,
+ [fn, userdata, project](bool modified) {
+ invoke(fn, userdata, OAKENGINE_EVENT_PROJECT_MODIFIED_CHANGED,
+ project, modified ? 1 : 0, 0, nullptr);
+ },
+ Qt::DirectConnection));
+ break;
+ }
+ case OAKENGINE_EVENT_PROJECT_NAME_CHANGED: {
+ auto *project = dynamic_cast(obj);
+ if (!project) {
+ break;
+ }
+ conns.append(QObject::connect(
+ project, &Project::name_changed, project,
+ [fn, userdata, project]() {
+ invoke(fn, userdata, OAKENGINE_EVENT_PROJECT_NAME_CHANGED,
+ project, 0, 0, nullptr);
+ },
+ Qt::DirectConnection));
+ break;
+ }
+ case OAKENGINE_EVENT_FOLDER_BEGIN_INSERT_ITEM:
+ case OAKENGINE_EVENT_FOLDER_END_INSERT_ITEM:
+ case OAKENGINE_EVENT_FOLDER_BEGIN_REMOVE_ITEM:
+ case OAKENGINE_EVENT_FOLDER_END_REMOVE_ITEM: {
+ auto *folder = dynamic_cast(obj);
+ if (!folder) {
+ break;
+ }
+ if (event_id == OAKENGINE_EVENT_FOLDER_BEGIN_INSERT_ITEM) {
+ conns.append(QObject::connect(
+ folder, &Folder::begin_insert_item, folder,
+ [fn, userdata, folder](Node *child, int index) {
+ invoke(fn, userdata, OAKENGINE_EVENT_FOLDER_BEGIN_INSERT_ITEM,
+ folder, index, 0, child);
+ },
+ Qt::DirectConnection));
+ } else if (event_id == OAKENGINE_EVENT_FOLDER_END_INSERT_ITEM) {
+ conns.append(QObject::connect(
+ folder, &Folder::end_insert_item, folder,
+ [fn, userdata, folder]() {
+ invoke(fn, userdata, OAKENGINE_EVENT_FOLDER_END_INSERT_ITEM,
+ folder, 0, 0, nullptr);
+ },
+ Qt::DirectConnection));
+ } else if (event_id == OAKENGINE_EVENT_FOLDER_BEGIN_REMOVE_ITEM) {
+ conns.append(QObject::connect(
+ folder, &Folder::begin_remove_item, folder,
+ [fn, userdata, folder](Node *child, int index) {
+ invoke(fn, userdata, OAKENGINE_EVENT_FOLDER_BEGIN_REMOVE_ITEM,
+ folder, index, 0, child);
+ },
+ Qt::DirectConnection));
+ } else {
+ conns.append(QObject::connect(
+ folder, &Folder::end_remove_item, folder,
+ [fn, userdata, folder]() {
+ invoke(fn, userdata, OAKENGINE_EVENT_FOLDER_END_REMOVE_ITEM,
+ folder, 0, 0, nullptr);
+ },
+ Qt::DirectConnection));
+ }
+ break;
+ }
+ case OAKENGINE_EVENT_SEQUENCE_TRACK_ADDED:
+ case OAKENGINE_EVENT_SEQUENCE_TRACK_REMOVED: {
+ auto *seq = dynamic_cast(obj);
+ if (!seq) {
+ break;
+ }
+ if (event_id == OAKENGINE_EVENT_SEQUENCE_TRACK_ADDED) {
+ conns.append(QObject::connect(
+ seq, &Sequence::track_added, seq,
+ [fn, userdata, seq](Track *track) {
+ invoke(fn, userdata, OAKENGINE_EVENT_SEQUENCE_TRACK_ADDED,
+ seq, track ? int(track->type()) : -1, 0, track);
+ },
+ Qt::DirectConnection));
+ } else {
+ conns.append(QObject::connect(
+ seq, &Sequence::track_removed, seq,
+ [fn, userdata, seq](Track *track) {
+ invoke(fn, userdata, OAKENGINE_EVENT_SEQUENCE_TRACK_REMOVED,
+ seq, track ? int(track->type()) : -1, 0, track);
+ },
+ Qt::DirectConnection));
+ }
+ break;
+ }
+ case OAKENGINE_EVENT_TRACK_BLOCK_ADDED:
+ case OAKENGINE_EVENT_TRACK_BLOCK_REMOVED: {
+ auto *track = dynamic_cast