diff --git a/.github/workflows/cd.yml b/.github/workflows/cd.yml index 9e66b425c..7ecb57bb4 100644 --- a/.github/workflows/cd.yml +++ b/.github/workflows/cd.yml @@ -67,10 +67,12 @@ jobs: # The FFmpeg bridge DLL is built outside app/ and is not reachable # via PATH, so ntldd cannot discover it from the executables alone cp build/ffmpeg_bridge/bin/ffmpeg_bridge.dll app/packaging/windows/nsis/oak-editor/ + # Same for the liboakcore DLL + cp build/core/oakcore.dll app/packaging/windows/nsis/oak-editor/ windeployqt6 app/packaging/windows/nsis/oak-editor/oak-editor.exe # Copy all non-Qt MSYS2 DLLs recursively for every binary we ship cd app/packaging/windows/nsis/oak-editor - for binary in oak-editor.exe oak-render-worker.exe oakgl.dll oakvulkan.dll ffmpeg_bridge.dll; do + for binary in oak-editor.exe oak-render-worker.exe oakgl.dll oakvulkan.dll ffmpeg_bridge.dll oakcore.dll; do [ -f "$binary" ] || continue for l in $(ntldd -R "$binary" | grep -E 'mingw64|ucrt64|clang64' | sed 's/^[ \t]*//' | cut -d' ' -f3); do cp -v "$l" . @@ -369,9 +371,11 @@ jobs: ls AppDir/usr/lib/liboakvulkan.so || (echo "Missing liboakvulkan.so" && exit 1) ls AppDir/usr/lib/libvulkan.so* || (echo "Missing libvulkan" && exit 1) ls AppDir/usr/lib/libffmpeg_bridge.so || (echo "Missing libffmpeg_bridge.so" && exit 1) + ls AppDir/usr/lib/liboakcore.so || (echo "Missing liboakcore.so" && exit 1) # Ensure every shipped binary resolves all of its dependencies ! ldd AppDir/usr/bin/oak-editor | grep "not found" ! ldd AppDir/usr/bin/oak-render-worker | grep "not found" + ! ldd AppDir/usr/lib/liboakcore.so | grep "not found" ! ldd AppDir/usr/lib/liboakgl.so | grep "not found" ! ldd AppDir/usr/lib/liboakvulkan.so | grep "not found" diff --git a/app/CMakeLists.txt b/app/CMakeLists.txt index 3ac4f2086..3d1c7bdc7 100644 --- a/app/CMakeLists.txt +++ b/app/CMakeLists.txt @@ -295,7 +295,8 @@ elseif (APPLE) COMMAND ${CMAKE_COMMAND} -E copy_if_different $ $/Contents/MacOS/ COMMAND ${CMAKE_COMMAND} -E copy_if_different $ $/Contents/MacOS/ COMMAND ${CMAKE_COMMAND} -E copy_if_different $ $/Contents/MacOS/ - COMMENT "Copying oak-render-worker, render backends, and ffmpeg_bridge into Oak.app" + COMMAND ${CMAKE_COMMAND} -E copy_if_different $ $/Contents/MacOS/ + COMMENT "Copying oak-render-worker, render backends, ffmpeg_bridge and liboakcore into Oak.app" ) if (TARGET oakvulkan) add_custom_command(TARGET olive-editor POST_BUILD @@ -307,6 +308,14 @@ elseif (UNIX) install(TARGETS olive-editor RUNTIME DESTINATION bin) endif () +if (WIN32) + # Windows has no RPATH: shared libraries must sit next to the executable + add_custom_command(TARGET olive-editor POST_BUILD + COMMAND ${CMAKE_COMMAND} -E copy_if_different $ $ + COMMAND ${CMAKE_COMMAND} -E copy_if_different $ $ + ) +endif () + # Set link libraries target_link_libraries(olive-editor PRIVATE ${OLIVE_LIBRARIES}) target_link_libraries(libolive-editor PRIVATE ${OLIVE_LIBRARIES}) diff --git a/app/timeline/timelineworkarea.cpp b/app/timeline/timelineworkarea.cpp index 0b2a5db90..92433a384 100644 --- a/app/timeline/timelineworkarea.cpp +++ b/app/timeline/timelineworkarea.cpp @@ -106,17 +106,17 @@ void TimelineWorkArea::save(QXmlStreamWriter *writer) const QString::fromStdString(this->out().to_string())); } -const Rational &TimelineWorkArea::in() const +Rational TimelineWorkArea::in() const { return workarea_range_.in(); } -const Rational &TimelineWorkArea::out() const +Rational TimelineWorkArea::out() const { return workarea_range_.out(); } -const Rational &TimelineWorkArea::length() const +Rational TimelineWorkArea::length() const { return workarea_range_.length(); } diff --git a/app/timeline/timelineworkarea.h b/app/timeline/timelineworkarea.h index 17a16816a..fd6be0391 100644 --- a/app/timeline/timelineworkarea.h +++ b/app/timeline/timelineworkarea.h @@ -40,9 +40,9 @@ public: bool enabled() const; void set_enabled(bool e); - const Rational &in() const; - const Rational &out() const; - const Rational &length() const; + Rational in() const; + Rational out() const; + Rational length() const; const TimeRange &range() const; void set_range(const TimeRange &range); diff --git a/core/CMakeLists.txt b/core/CMakeLists.txt index 48e6e9201..4ec6f5017 100644 --- a/core/CMakeLists.txt +++ b/core/CMakeLists.txt @@ -19,7 +19,7 @@ cmake_minimum_required(VERSION 3.13 FATAL_ERROR) project(libolivecore VERSION 1.0.0 LANGUAGES CXX) -option(OLIVECORE_BUILD_TESTS ON) +option(OLIVECORE_BUILD_TESTS "Build liboakcore tests" ON) set(CMAKE_CXX_STANDARD 17) set(CMAKE_CXX_STANDARD_REQUIRED ON) @@ -36,7 +36,20 @@ if(UNIX AND NOT APPLE AND NOT DEFINED OpenGL_GL_PREFERENCE) endif() find_package(OpenGL REQUIRED) -add_library(olivecore +# liboakcore is a shared library. It exports a pure C ABI (see +# include/olive/core/oakcore/): every public function is declared with +# OAKCORE_API and all C++ symbols are hidden, so no C++ ABI crosses the +# library boundary. Consumers use the wrapper headers in include/olive/core. +add_library(olivecore SHARED + src/capi/audioparams.cpp + src/capi/bezier.cpp + src/capi/color.cpp + src/capi/fractionutils.cpp + src/capi/rational.cpp + src/capi/samplebuffer.cpp + src/capi/stringutils.cpp + src/capi/timecodefunctions.cpp + src/capi/timerange.cpp src/render/audioparams.cpp src/render/samplebuffer.cpp src/util/bezier.cpp @@ -50,7 +63,23 @@ add_library(olivecore src/util/value.cpp ) +set_target_properties(olivecore PROPERTIES + OUTPUT_NAME oakcore + POSITION_INDEPENDENT_CODE ON + C_VISIBILITY_PRESET hidden + CXX_VISIBILITY_PRESET hidden + VISIBILITY_INLINES_HIDDEN ON +) + +# OAKCORE_BUILD marks the library side of the export macros (dllexport) +target_compile_definitions(olivecore PRIVATE OAKCORE_BUILD) + +# The library builds against its internal implementation headers +# (src/oliveimpl); consumers only ever see the public C API and wrapper +# headers (include/olive/core). oliveimpl must come first so that internal +# sources never pick up a wrapper header by accident. target_include_directories(olivecore PRIVATE + "${CMAKE_CURRENT_SOURCE_DIR}/src/oliveimpl" "${CMAKE_CURRENT_SOURCE_DIR}/include/olive/core" "${CMAKE_SOURCE_DIR}/third_party/openfx/include/" ) @@ -70,8 +99,19 @@ else() message(" OpenTimelineIO interchange will be disabled.") endif() -install(TARGETS olivecore) -install(DIRECTORY "${CMAKE_CURRENT_SOURCE_DIR}/include/olive" DESTINATION "${CMAKE_INSTALL_PREFIX}/include") +# liboakcore installs into the platform's standard library directory +# (/usr/lib, /usr/lib64 or the Debian multiarch path), not a custom one. +# Windows DLLs go next to the executables in bin. +include(GNUInstallDirs) +if (WIN32) + install(TARGETS olivecore + RUNTIME DESTINATION bin + ARCHIVE DESTINATION ${CMAKE_INSTALL_LIBDIR}) +else () + install(TARGETS olivecore + LIBRARY DESTINATION ${CMAKE_INSTALL_LIBDIR} + ARCHIVE DESTINATION ${CMAKE_INSTALL_LIBDIR}) +endif () if (OLIVECORE_BUILD_TESTS) enable_testing() @@ -82,13 +122,20 @@ if (OLIVECORE_BUILD_TESTS) ) target_link_libraries(${name} PRIVATE olivecore) target_include_directories(${name} PRIVATE + "${CMAKE_CURRENT_SOURCE_DIR}/include" "${CMAKE_CURRENT_SOURCE_DIR}/include/olive/core" ) add_test(${name} ${name}) endfunction() - make_test(rational-test) - make_test(stringutils-test) - make_test(timecode-test) - make_test(timerange-test) + # Pure C ABI tests for the liboakcore public interface + make_test(oakcore_audioparams_test) + make_test(oakcore_bezier_test) + make_test(oakcore_color_test) + make_test(oakcore_fractionutils_test) + make_test(oakcore_rational_test) + make_test(oakcore_samplebuffer_test) + make_test(oakcore_stringutils_test) + make_test(oakcore_timecodefunctions_test) + make_test(oakcore_timerange_test) endif() diff --git a/core/include/olive/core/core.h b/core/include/olive/core/core.h index 7d948523c..c58e988f2 100644 --- a/core/include/olive/core/core.h +++ b/core/include/olive/core/core.h @@ -33,6 +33,7 @@ #include "util/tests.h" #include "util/timecodefunctions.h" #include "util/timerange.h" -#include "util/value.h" +// util/value.h is deliberately not part of the public API: the generic +// Value container is unused by consumers and stays internal to the library #endif // OAK_LIBOLIVECORE_H diff --git a/core/include/olive/core/oakcore/audioparams.h b/core/include/olive/core/oakcore/audioparams.h new file mode 100644 index 000000000..1c09c8d4f --- /dev/null +++ b/core/include/olive/core/oakcore/audioparams.h @@ -0,0 +1,157 @@ +/*** + + 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 OAKCORE_AUDIOPARAMS_H +#define OAKCORE_AUDIOPARAMS_H + +#include + +#include "export.h" +#include "rational.h" + +#ifdef __cplusplus +extern "C" { +#endif + +/** + * @file audioparams.h + * @brief C ABI for the audio stream parameter value type + * + * Opaque handle + free functions. All returned OakAudioParams* and + * OakRational* are owned by the caller and must be released with + * oakcore_audioparams_free() / oakcore_rational_free() respectively. + * + * Sample formats cross the boundary as plain ints carrying the + * olive::core::SampleFormat::Format enum values (render/sampleformat.h); + * channel layouts are plain 64-bit masks (render/channellayout.h). + */ +typedef struct OakAudioParams OakAudioParams; + +/** + * Creates parameters for the given sample rate, channel layout mask and + * sample format. The timebase is initialized to 1/sample_rate. + */ +OAKCORE_API OakAudioParams *oakcore_audioparams_create(int sample_rate, + uint64_t channel_layout, + int format); + +/** + * Creates default (invalid) parameters: sample_rate=0, empty channel + * layout, invalid format. + */ +OAKCORE_API OakAudioParams *oakcore_audioparams_create_invalid(void); + +OAKCORE_API OakAudioParams *oakcore_audioparams_copy(const OakAudioParams *self); +OAKCORE_API void oakcore_audioparams_free(OakAudioParams *self); + +OAKCORE_API int oakcore_audioparams_sample_rate(const OakAudioParams *self); +OAKCORE_API void oakcore_audioparams_set_sample_rate(OakAudioParams *self, + int sample_rate); + +OAKCORE_API uint64_t oakcore_audioparams_channel_layout(const OakAudioParams *self); +OAKCORE_API void oakcore_audioparams_set_channel_layout(OakAudioParams *self, + uint64_t mask); + +OAKCORE_API OakRational *oakcore_audioparams_time_base(const OakAudioParams *self); +OAKCORE_API void oakcore_audioparams_set_time_base(OakAudioParams *self, + const OakRational *timebase); + +/** + * Returns a new owned handle to Rational(1, sample_rate). + */ +OAKCORE_API OakRational *oakcore_audioparams_sample_rate_as_time_base( + const OakAudioParams *self); + +OAKCORE_API int oakcore_audioparams_format(const OakAudioParams *self); +OAKCORE_API void oakcore_audioparams_set_format(OakAudioParams *self, int format); + +OAKCORE_API int oakcore_audioparams_enabled(const OakAudioParams *self); +OAKCORE_API void oakcore_audioparams_set_enabled(OakAudioParams *self, int enabled); + +OAKCORE_API int oakcore_audioparams_stream_index(const OakAudioParams *self); +OAKCORE_API void oakcore_audioparams_set_stream_index(OakAudioParams *self, + int stream_index); + +OAKCORE_API int64_t oakcore_audioparams_duration(const OakAudioParams *self); +OAKCORE_API void oakcore_audioparams_set_duration(OakAudioParams *self, + int64_t duration); + +/** + * Time/byte/sample conversions. All of these require valid parameters + * (oakcore_audioparams_is_valid()); calling them on invalid parameters is + * an error (the library asserts in debug builds). + */ +OAKCORE_API int64_t oakcore_audioparams_time_to_bytes(const OakAudioParams *self, + double time); +OAKCORE_API int64_t oakcore_audioparams_time_to_bytes_rational( + const OakAudioParams *self, const OakRational *time); +OAKCORE_API int64_t oakcore_audioparams_time_to_bytes_per_channel( + const OakAudioParams *self, double time); +OAKCORE_API int64_t oakcore_audioparams_time_to_bytes_per_channel_rational( + const OakAudioParams *self, const OakRational *time); +OAKCORE_API int64_t oakcore_audioparams_time_to_samples(const OakAudioParams *self, + double time); +OAKCORE_API int64_t oakcore_audioparams_time_to_samples_rational( + const OakAudioParams *self, const OakRational *time); +OAKCORE_API int64_t oakcore_audioparams_samples_to_bytes(const OakAudioParams *self, + int64_t samples); +OAKCORE_API int64_t oakcore_audioparams_samples_to_bytes_per_channel( + const OakAudioParams *self, int64_t samples); +OAKCORE_API OakRational *oakcore_audioparams_samples_to_time( + const OakAudioParams *self, int64_t samples); +OAKCORE_API int64_t oakcore_audioparams_bytes_to_samples(const OakAudioParams *self, + int64_t bytes); +OAKCORE_API OakRational *oakcore_audioparams_bytes_to_time( + const OakAudioParams *self, int64_t bytes); +OAKCORE_API OakRational *oakcore_audioparams_bytes_per_channel_to_time( + const OakAudioParams *self, int64_t bytes); + +OAKCORE_API int oakcore_audioparams_channel_count(const OakAudioParams *self); +OAKCORE_API int oakcore_audioparams_bytes_per_sample_per_channel( + const OakAudioParams *self); +OAKCORE_API int oakcore_audioparams_bits_per_sample(const OakAudioParams *self); +OAKCORE_API int oakcore_audioparams_is_valid(const OakAudioParams *self); + +/** + * Equality like operator==: 1 when equal, 0 otherwise. Compares format, + * sample rate, timebase and channel layout (not the footage parameters). + */ +OAKCORE_API int oakcore_audioparams_equals(const OakAudioParams *self, + const OakAudioParams *other); + +/** + * Enumerates AudioParams::k_supported_channel_layouts. Out-of-range indices + * return 0. + */ +OAKCORE_API int oakcore_audioparams_supported_channel_layout_count(void); +OAKCORE_API uint64_t oakcore_audioparams_supported_channel_layout_at(int index); + +/** + * Enumerates AudioParams::k_supported_sample_rates. Out-of-range indices + * return 0. + */ +OAKCORE_API int oakcore_audioparams_supported_sample_rate_count(void); +OAKCORE_API int oakcore_audioparams_supported_sample_rate_at(int index); + +#ifdef __cplusplus +} +#endif + +#endif /* OAKCORE_AUDIOPARAMS_H */ diff --git a/core/include/olive/core/oakcore/bezier.h b/core/include/olive/core/oakcore/bezier.h new file mode 100644 index 000000000..5ece83f90 --- /dev/null +++ b/core/include/olive/core/oakcore/bezier.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 OAKCORE_BEZIER_H +#define OAKCORE_BEZIER_H + +#include "export.h" + +#ifdef __cplusplus +extern "C" { +#endif + +/** + * @file bezier.h + * @brief C ABI for the keyframe easing bezier curve value type + * + * Opaque handle + free functions. All returned OakBezier* are owned by + * the caller and must be released with oakcore_bezier_free(). + */ +typedef struct OakBezier OakBezier; + +OAKCORE_API OakBezier *oakcore_bezier_create(void); +OAKCORE_API OakBezier *oakcore_bezier_create_xy(double x, double y); +OAKCORE_API OakBezier *oakcore_bezier_create_full(double x, double y, + double cp1_x, double cp1_y, + double cp2_x, double cp2_y); +OAKCORE_API OakBezier *oakcore_bezier_copy(const OakBezier *self); +OAKCORE_API void oakcore_bezier_free(OakBezier *self); + +OAKCORE_API double oakcore_bezier_x(const OakBezier *self); +OAKCORE_API double oakcore_bezier_y(const OakBezier *self); +OAKCORE_API double oakcore_bezier_cp1_x(const OakBezier *self); +OAKCORE_API double oakcore_bezier_cp1_y(const OakBezier *self); +OAKCORE_API double oakcore_bezier_cp2_x(const OakBezier *self); +OAKCORE_API double oakcore_bezier_cp2_y(const OakBezier *self); + +OAKCORE_API void oakcore_bezier_set_x(OakBezier *self, double x); +OAKCORE_API void oakcore_bezier_set_y(OakBezier *self, double y); +OAKCORE_API void oakcore_bezier_set_cp1_x(OakBezier *self, double cp1_x); +OAKCORE_API void oakcore_bezier_set_cp1_y(OakBezier *self, double cp1_y); +OAKCORE_API void oakcore_bezier_set_cp2_x(OakBezier *self, double cp2_x); +OAKCORE_API void oakcore_bezier_set_cp2_y(OakBezier *self, double cp2_y); + +OAKCORE_API double oakcore_bezier_quadratic_xto_t(double x, double a, double b, + double c); +OAKCORE_API double oakcore_bezier_quadratic_tto_y(double a, double b, double c, + double t); +OAKCORE_API double oakcore_bezier_cubic_xto_t(double x, double a, double b, + double c, double d); +OAKCORE_API double oakcore_bezier_cubic_tto_y(double a, double b, double c, + double d, double t); + +#ifdef __cplusplus +} +#endif + +#endif /* OAKCORE_BEZIER_H */ diff --git a/core/include/olive/core/oakcore/color.h b/core/include/olive/core/oakcore/color.h new file mode 100644 index 000000000..61734f162 --- /dev/null +++ b/core/include/olive/core/oakcore/color.h @@ -0,0 +1,123 @@ +/*** + + 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 OAKCORE_COLOR_H +#define OAKCORE_COLOR_H + +#include "export.h" + +#ifdef __cplusplus +extern "C" { +#endif + +/** + * @file color.h + * @brief C ABI for the high precision RGBA color value type + * + * Opaque handle + free functions. All returned OakColor* are owned by the + * caller and must be released with oakcore_color_free(). + * + * A color is always 4 float channels in red/green/blue/alpha order. Channel + * values are unbounded: values outside [0, 1] are valid (HDR). + * + * Functions taking a `format` argument expect the same values as + * PixelFormat::Format (render/pixelformat.h): invalid = -1, u8 = 0, u10 = 1, + * u16 = 2, f16 = 3, f32 = 4. + */ +typedef struct OakColor OakColor; + +OAKCORE_API OakColor *oakcore_color_create(void); +OAKCORE_API OakColor *oakcore_color_create_rgba(float r, float g, float b, + float a); +OAKCORE_API OakColor *oakcore_color_copy(const OakColor *self); +OAKCORE_API void oakcore_color_free(OakColor *self); + +/** + * Creates a color from hue/saturation/value. + * + * Hue expects a value between 0.0 and 360.0. Saturation and value expect a + * value between 0.0 and 1.0. + */ +OAKCORE_API OakColor *oakcore_color_from_hsv(float h, float s, float v); + +/** + * Creates a color from raw pixel data in the given pixel format. At most 4 + * channels are read; missing channels default to 0. + */ +OAKCORE_API OakColor *oakcore_color_from_data(const char *data, int format, + int nb_channels); + +OAKCORE_API float oakcore_color_red(const OakColor *self); +OAKCORE_API float oakcore_color_green(const OakColor *self); +OAKCORE_API float oakcore_color_blue(const OakColor *self); +OAKCORE_API float oakcore_color_alpha(const OakColor *self); + +OAKCORE_API void oakcore_color_set_red(OakColor *self, float red); +OAKCORE_API void oakcore_color_set_green(OakColor *self, float green); +OAKCORE_API void oakcore_color_set_blue(OakColor *self, float blue); +OAKCORE_API void oakcore_color_set_alpha(OakColor *self, float alpha); + +OAKCORE_API void oakcore_color_to_hsv(const OakColor *self, float *hue, + float *sat, float *val); +OAKCORE_API float oakcore_color_hsv_hue(const OakColor *self); +OAKCORE_API float oakcore_color_hsv_saturation(const OakColor *self); +OAKCORE_API float oakcore_color_value(const OakColor *self); + +OAKCORE_API void oakcore_color_to_hsl(const OakColor *self, float *hue, + float *sat, float *lightness); +OAKCORE_API float oakcore_color_hsl_hue(const OakColor *self); +OAKCORE_API float oakcore_color_hsl_saturation(const OakColor *self); +OAKCORE_API float oakcore_color_lightness(const OakColor *self); + +/** + * Borrowed pointer to the 4 float channels (rgba order), valid until + * oakcore_color_free(). + */ +OAKCORE_API float *oakcore_color_data(OakColor *self); +OAKCORE_API const float *oakcore_color_const_data(const OakColor *self); + +/** + * Writes the color as raw pixel data in the given pixel format. At most 4 + * channels are written; out must have room for nb_channels * bytes-per-channel + * (u10 always packs to 4 bytes). + */ +OAKCORE_API void oakcore_color_to_data(const OakColor *self, char *out, + int format, int nb_channels); + +/** + * Super rough luminance value mostly used for UI (determining whether to + * overlay with black or white text). + */ +OAKCORE_API float oakcore_color_get_rough_luminance(const OakColor *self); + +OAKCORE_API void oakcore_color_add_assign(OakColor *self, + const OakColor *other); +OAKCORE_API void oakcore_color_sub_assign(OakColor *self, + const OakColor *other); +OAKCORE_API void oakcore_color_add_scalar_assign(OakColor *self, float value); +OAKCORE_API void oakcore_color_sub_scalar_assign(OakColor *self, float value); +OAKCORE_API void oakcore_color_mul_scalar_assign(OakColor *self, float value); +OAKCORE_API void oakcore_color_div_scalar_assign(OakColor *self, float value); + +#ifdef __cplusplus +} +#endif + +#endif /* OAKCORE_COLOR_H */ diff --git a/core/include/olive/core/oakcore/export.h b/core/include/olive/core/oakcore/export.h new file mode 100644 index 000000000..c17654f11 --- /dev/null +++ b/core/include/olive/core/oakcore/export.h @@ -0,0 +1,45 @@ +/*** + + 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 OAKCORE_EXPORT_H +#define OAKCORE_EXPORT_H + +/** + * @file export.h + * @brief Symbol visibility macros for liboakcore + * + * liboakcore exposes a pure C ABI: every public function is declared with + * OAKCORE_API and everything else is hidden. No C++ symbols cross the + * library boundary. + */ + +#if defined(_WIN32) || defined(__CYGWIN__) + #ifdef OAKCORE_BUILD + #define OAKCORE_API __declspec(dllexport) + #else + #define OAKCORE_API __declspec(dllimport) + #endif +#elif defined(__GNUC__) || defined(__clang__) + #define OAKCORE_API __attribute__((visibility("default"))) +#else + #define OAKCORE_API +#endif + +#endif /* OAKCORE_EXPORT_H */ diff --git a/core/include/olive/core/oakcore/fractionutils.h b/core/include/olive/core/oakcore/fractionutils.h new file mode 100644 index 000000000..550370663 --- /dev/null +++ b/core/include/olive/core/oakcore/fractionutils.h @@ -0,0 +1,88 @@ +/*** + + 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 OAKCORE_FRACTIONUTILS_H +#define OAKCORE_FRACTIONUTILS_H + +#include + +#include "export.h" + +#ifdef __cplusplus +extern "C" { +#endif + +/** + * @file fractionutils.h + * @brief C ABI for the fraction utility functions + * + * Pure integer helpers: there is no object and therefore no opaque handle. + * In/out values are passed through pointer out-parameters. + */ + +/** + * @brief Rounding modes for oakcore_fractionutils_rescale_rnd() + * + * Mirrors the FFmpeg AVRounding modes: NEAR_INF rounds to the nearest value + * with halfway cases rounded away from zero, UP rounds toward positive + * infinity. + */ +typedef enum OakFractionRounding { + OAK_FRACTION_ROUNDING_NEAR_INF = 0, + OAK_FRACTION_ROUNDING_UP = 1 +} OakFractionRounding; + +/** + * @brief Reduces the fraction *num / *den in place so that both fit within max + * + * Divides out the greatest common divisor and, if the values still do not + * fit within max, finds the closest approximation using continued fractions. + * + * A zero denominator is preserved (with the numerator set to zero). + * num and den must not be NULL. + */ +OAKCORE_API void oakcore_fractionutils_reduce_fraction(int64_t *num, + int64_t *den, + int64_t max); + +/** + * @brief Three-way comparison of the fractions an/ad and bn/bd + * + * Returns -1 if a < b, 0 if a == b, 1 if a > b, and INT_MIN when the + * comparison is meaningless (degenerate zero-denominator fractions). + */ +OAKCORE_API int oakcore_fractionutils_compare_fractions(int an, int ad, + int bn, int bd); + +/** + * @brief Rescales a by the fraction b/c: returns a * b / c + * + * The intermediate product is computed with 128-bit arithmetic where + * available so that no precision is lost for large timestamps. + */ +OAKCORE_API int64_t oakcore_fractionutils_rescale_rnd(int64_t a, int64_t b, + int64_t c, + OakFractionRounding rnd); + +#ifdef __cplusplus +} +#endif + +#endif /* OAKCORE_FRACTIONUTILS_H */ diff --git a/core/include/olive/core/oakcore/rational.h b/core/include/olive/core/oakcore/rational.h new file mode 100644 index 000000000..456d94980 --- /dev/null +++ b/core/include/olive/core/oakcore/rational.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 OAKCORE_RATIONAL_H +#define OAKCORE_RATIONAL_H + +#include "export.h" + +#ifdef __cplusplus +extern "C" { +#endif + +/** + * @file rational.h + * @brief C ABI for the rational number value type + * + * Opaque handle + free functions. All returned OakRational* are owned by + * the caller and must be released with oakcore_rational_free(). + */ +typedef struct OakRational OakRational; + +OAKCORE_API OakRational *oakcore_rational_create(int numerator); +OAKCORE_API OakRational *oakcore_rational_create_nd(int numerator, + int denominator); +OAKCORE_API OakRational *oakcore_rational_create_nan(void); +OAKCORE_API OakRational *oakcore_rational_copy(const OakRational *self); +OAKCORE_API void oakcore_rational_free(OakRational *self); + +OAKCORE_API int oakcore_rational_numerator(const OakRational *self); +OAKCORE_API int oakcore_rational_denominator(const OakRational *self); +OAKCORE_API double oakcore_rational_to_double(const OakRational *self); + +/** + * Writes "num/den" into buf (NUL-terminated when buf_size > 0). + * Returns the number of characters that would have been written excluding + * the NUL, so buf == NULL or a too-small buffer can be used to query the + * required size. + */ +OAKCORE_API int oakcore_rational_to_string(const OakRational *self, char *buf, + int buf_size); + +OAKCORE_API OakRational *oakcore_rational_from_double(double value, int *ok); +OAKCORE_API OakRational *oakcore_rational_from_string(const char *str, int *ok); + +OAKCORE_API int oakcore_rational_is_null(const OakRational *self); +OAKCORE_API int oakcore_rational_is_nan(const OakRational *self); + +OAKCORE_API OakRational *oakcore_rational_flipped(const OakRational *self); +OAKCORE_API void oakcore_rational_flip(OakRational *self); + +OAKCORE_API void oakcore_rational_add_assign(OakRational *self, + const OakRational *other); +OAKCORE_API void oakcore_rational_sub_assign(OakRational *self, + const OakRational *other); +OAKCORE_API void oakcore_rational_mul_assign(OakRational *self, + const OakRational *other); +OAKCORE_API void oakcore_rational_div_assign(OakRational *self, + const OakRational *other); + +/** + * Three-way comparison like compare_fractions: -1, 0 or 1. + */ +OAKCORE_API int oakcore_rational_compare(const OakRational *self, + const OakRational *other); + +#ifdef __cplusplus +} +#endif + +#endif /* OAKCORE_RATIONAL_H */ diff --git a/core/include/olive/core/oakcore/samplebuffer.h b/core/include/olive/core/oakcore/samplebuffer.h new file mode 100644 index 000000000..5472e2274 --- /dev/null +++ b/core/include/olive/core/oakcore/samplebuffer.h @@ -0,0 +1,149 @@ +/*** + + 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 OAKCORE_SAMPLEBUFFER_H +#define OAKCORE_SAMPLEBUFFER_H + +#include + +#include "export.h" +#include "rational.h" + +#ifdef __cplusplus +extern "C" { +#endif + +/** + * @file samplebuffer.h + * @brief C ABI for the planar audio sample buffer + * + * Opaque handle + free functions. Every returned OakSampleBuffer or + * OakAudioParams handle is owned by the caller and must be released with + * oakcore_samplebuffer_free() or oakcore_audioparams_free() respectively. + * Samples are always stored planar (one float array per channel). + */ +typedef struct OakSampleBuffer OakSampleBuffer; +typedef struct OakAudioParams OakAudioParams; + +OAKCORE_API OakSampleBuffer *oakcore_samplebuffer_create(void); +OAKCORE_API OakSampleBuffer *oakcore_samplebuffer_create_length( + const OakAudioParams *params, const OakRational *length); +OAKCORE_API OakSampleBuffer *oakcore_samplebuffer_create_samples( + const OakAudioParams *params, size_t samples_per_channel); +OAKCORE_API OakSampleBuffer *oakcore_samplebuffer_copy( + const OakSampleBuffer *self); +OAKCORE_API void oakcore_samplebuffer_free(OakSampleBuffer *self); + +OAKCORE_API OakSampleBuffer *oakcore_samplebuffer_rip_channel( + const OakSampleBuffer *self, int channel); + +/** + * Copies the samples of one channel into out. Returns the total number of + * floats in the channel (equal to the sample count), so out == NULL (or a + * too-small buffer) can be used to query the required size; at most out_size + * floats are written. + */ +OAKCORE_API int oakcore_samplebuffer_rip_channel_vector( + const OakSampleBuffer *self, int channel, float *out, int out_size); + +/** + * Returns a copy of the buffer's audio parameters as a new owned handle + * (release with oakcore_audioparams_free()). + */ +OAKCORE_API OakAudioParams *oakcore_samplebuffer_audio_params( + const OakSampleBuffer *self); +OAKCORE_API void oakcore_samplebuffer_set_audio_params( + OakSampleBuffer *self, const OakAudioParams *params); + +OAKCORE_API size_t oakcore_samplebuffer_sample_count( + const OakSampleBuffer *self); +OAKCORE_API void oakcore_samplebuffer_set_sample_count(OakSampleBuffer *self, + size_t sample_count); +OAKCORE_API void oakcore_samplebuffer_set_sample_count_length( + OakSampleBuffer *self, const OakRational *length); + +/** + * Borrowed pointer to the samples of one channel; it becomes invalid when the + * handle is destroyed or reallocated and must not be freed by the caller. + * Returns NULL when the buffer is not allocated or the channel is out of + * range. + */ +OAKCORE_API float *oakcore_samplebuffer_data(OakSampleBuffer *self, + int channel); + +/** + * Fills out with one borrowed sample pointer per channel (see + * oakcore_samplebuffer_data()). out must have room for + * oakcore_samplebuffer_channel_count() entries. + */ +OAKCORE_API void oakcore_samplebuffer_to_raw_ptrs(OakSampleBuffer *self, + float **out); + +OAKCORE_API int oakcore_samplebuffer_channel_count(const OakSampleBuffer *self); +OAKCORE_API int oakcore_samplebuffer_is_allocated(const OakSampleBuffer *self); +OAKCORE_API void oakcore_samplebuffer_allocate(OakSampleBuffer *self); +OAKCORE_API void oakcore_samplebuffer_destroy(OakSampleBuffer *self); + +OAKCORE_API void oakcore_samplebuffer_reverse(OakSampleBuffer *self); +OAKCORE_API void oakcore_samplebuffer_speed(OakSampleBuffer *self, + double speed); + +OAKCORE_API void oakcore_samplebuffer_transform_volume(OakSampleBuffer *self, + float f); +OAKCORE_API void oakcore_samplebuffer_transform_volume_for_channel( + OakSampleBuffer *self, int channel, float volume); +OAKCORE_API void oakcore_samplebuffer_transform_volume_to( + float f, const OakSampleBuffer *input, OakSampleBuffer *output); +OAKCORE_API void oakcore_samplebuffer_transform_volume_for_channel_to( + int channel, float volume, const OakSampleBuffer *input, + OakSampleBuffer *output); +OAKCORE_API void oakcore_samplebuffer_transform_volume_for_sample( + OakSampleBuffer *self, size_t sample_index, float volume); +OAKCORE_API void oakcore_samplebuffer_transform_volume_for_sample_on_channel( + OakSampleBuffer *self, size_t sample_index, int channel, float volume); + +OAKCORE_API void oakcore_samplebuffer_clamp(OakSampleBuffer *self); + +OAKCORE_API void oakcore_samplebuffer_silence(OakSampleBuffer *self); +OAKCORE_API void oakcore_samplebuffer_silence_range(OakSampleBuffer *self, + size_t start_sample, + size_t end_sample); +OAKCORE_API void oakcore_samplebuffer_silence_bytes(OakSampleBuffer *self, + size_t start_byte, + size_t end_byte); + +OAKCORE_API void oakcore_samplebuffer_set(OakSampleBuffer *self, int channel, + const float *data, + size_t sample_offset, + size_t sample_length); + +/** + * Copies channel "from" of other into channel "to" of self; from == -1 uses + * the same index as to. + */ +OAKCORE_API void oakcore_samplebuffer_fast_set(OakSampleBuffer *self, + const OakSampleBuffer *other, + int to, int from); + +#ifdef __cplusplus +} +#endif + +#endif /* OAKCORE_SAMPLEBUFFER_H */ diff --git a/core/include/olive/core/oakcore/stringutils.h b/core/include/olive/core/oakcore/stringutils.h new file mode 100644 index 000000000..8f4980e97 --- /dev/null +++ b/core/include/olive/core/oakcore/stringutils.h @@ -0,0 +1,94 @@ +/*** + + 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 OAKCORE_STRINGUTILS_H +#define OAKCORE_STRINGUTILS_H + +#include + +#include "export.h" + +#ifdef __cplusplus +extern "C" { +#endif + +/** + * @file stringutils.h + * @brief C ABI for the string utility functions + * + * StringUtils consists of static functions only, so there are no objects + * and no opaque handle. Functions returning string lists hand back a + * caller-owned char** that must be released with + * oakcore_stringutils_free_string_array(). + */ + +/** + * Splits s on separator and returns a newly allocated array of newly + * allocated NUL-terminated strings; *count receives the element count. + * Release the result with oakcore_stringutils_free_string_array(). + * A NULL or empty s yields one empty string, mirroring the C++ + * implementation. Returns NULL only on allocation failure. + */ +OAKCORE_API char **oakcore_stringutils_split(const char *s, char separator, + int *count); + +/** + * Splits s wherever the regular expression pattern matches (std::regex + * ECMAScript syntax). Same ownership and NULL semantics as + * oakcore_stringutils_split(). + */ +OAKCORE_API char **oakcore_stringutils_split_regex(const char *s, + const char *pattern, + int *count); + +/** + * Releases an array returned by oakcore_stringutils_split() or + * oakcore_stringutils_split_regex(). Safe to call with arr == NULL. + */ +OAKCORE_API void oakcore_stringutils_free_string_array(char **arr, int count); + +/** + * Parses an int from s in the given base (usually 10, or 16 for hex). + * Returns the parsed value, or 0 on parser error. ok is an optional output + * parameter set to 1 on success and 0 on failure. + */ +OAKCORE_API int oakcore_stringutils_to_int(const char *s, int base, int *ok); + +/** + * Formats a string with vsnprintf semantics: writes into buf + * (NUL-terminated when buf_size > 0) and returns the number of characters + * that would have been written excluding the NUL, so buf == NULL or a + * too-small buffer can be used to query the required size. + */ +OAKCORE_API int oakcore_stringutils_format(char *buf, int buf_size, + const char *fmt, ...); + +/** + * va_list form of oakcore_stringutils_format(), for forwarding from other + * variadic functions. + */ +OAKCORE_API int oakcore_stringutils_format_v(char *buf, int buf_size, + const char *fmt, va_list args); + +#ifdef __cplusplus +} +#endif + +#endif /* OAKCORE_STRINGUTILS_H */ diff --git a/core/include/olive/core/oakcore/timecodefunctions.h b/core/include/olive/core/oakcore/timecodefunctions.h new file mode 100644 index 000000000..56d21afb0 --- /dev/null +++ b/core/include/olive/core/oakcore/timecodefunctions.h @@ -0,0 +1,145 @@ +/*** + + 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 OAKCORE_TIMECODEFUNCTIONS_H +#define OAKCORE_TIMECODEFUNCTIONS_H + +#include + +#include "export.h" +#include "rational.h" + +#ifdef __cplusplus +extern "C" { +#endif + +/** + * @file timecodefunctions.h + * @brief C ABI for the time/timecode/timestamp conversion functions + * + * Free functions only (the wrapped class is all-static, so there is no + * object handle of its own). Times and timebases are passed as borrowed + * OakRational handles; every returned OakRational* is owned by the caller + * and must be released with oakcore_rational_free(). + * + * Terminology used throughout: + * + * `time` - time in seconds presented in a Rational form + * `timebase` - the base time unit of an audio/video stream in seconds + * `timestamp` - an integer representation of a time in timebase units + * (in many cases is used like a frame number) + * `timecode` - a user-friendly string representation of a time according to OakTimecodeDisplay + */ + +/** + * @brief User-friendly timecode display modes + * + * Values must stay in sync with the wrapped class's Display enum. + */ +typedef enum OakTimecodeDisplay { + OAK_TIMECODE_DISPLAY_DROP_FRAME = 0, + OAK_TIMECODE_DISPLAY_NON_DROP_FRAME = 1, + OAK_TIMECODE_DISPLAY_SECONDS = 2, + OAK_TIMECODE_DISPLAY_FRAMES = 3, + OAK_TIMECODE_DISPLAY_MILLISECONDS = 4 +} OakTimecodeDisplay; + +/** + * @brief Rounding modes for time/timestamp conversions + * + * Values must stay in sync with the wrapped class's Rounding enum. + */ +typedef enum OakTimecodeRounding { + OAK_TIMECODE_ROUNDING_CEIL = 0, + OAK_TIMECODE_ROUNDING_FLOOR = 1, + OAK_TIMECODE_ROUNDING_ROUND = 2 +} OakTimecodeRounding; + +/** + * @brief Convert a time (according to a Rational timebase) to a user-friendly string representation + * + * Writes the timecode into buf (NUL-terminated when buf_size > 0). + * Returns the number of characters that would have been written excluding + * the NUL, so buf == NULL or a too-small buffer can be used to query the + * required size. + */ +OAKCORE_API int oakcore_timecode_time_to_timecode(const OakRational *time, + const OakRational *timebase, + OakTimecodeDisplay display, + int show_plus_if_positive, + char *buf, int buf_size); + +/** + * @brief Convert a user-friendly timecode string to a time in seconds + * + * Returns a new owned OakRational (free with oakcore_rational_free()). + * ok is set to 1 on success and 0 on failure (may be NULL). + */ +OAKCORE_API OakRational *oakcore_timecode_timecode_to_time(const char *timecode, + const OakRational *timebase, + OakTimecodeDisplay display, + int *ok); + +/** + * @brief Convert a millisecond count to an "HH:MM:SS" string + * + * Same buf/buf_size convention as oakcore_timecode_time_to_timecode(). + */ +OAKCORE_API int oakcore_timecode_time_to_string(int64_t ms, char *buf, + int buf_size); + +/** + * @brief Snap a time to the nearest timestamp boundary of a timebase + * + * Returns a new owned OakRational (free with oakcore_rational_free()). + */ +OAKCORE_API OakRational *oakcore_timecode_snap_time_to_timebase(const OakRational *time, + const OakRational *timebase, + OakTimecodeRounding rounding); + +OAKCORE_API int64_t oakcore_timecode_time_to_timestamp(const OakRational *time, + const OakRational *timebase, + OakTimecodeRounding rounding); +OAKCORE_API int64_t oakcore_timecode_time_to_timestamp_d(double time, + const OakRational *timebase, + OakTimecodeRounding rounding); + +OAKCORE_API int64_t oakcore_timecode_rescale_timestamp(int64_t ts, + const OakRational *source, + const OakRational *dest); +OAKCORE_API int64_t oakcore_timecode_rescale_timestamp_ceil(int64_t ts, + const OakRational *source, + const OakRational *dest); + +/** + * @brief Convert a timestamp in timebase units back to a time in seconds + * + * Returns a new owned OakRational (free with oakcore_rational_free()). + */ +OAKCORE_API OakRational *oakcore_timecode_timestamp_to_time(int64_t timestamp, + const OakRational *timebase); + +OAKCORE_API int oakcore_timecode_timebase_is_drop_frame(const OakRational *timebase); + +#ifdef __cplusplus +} +#endif + +#endif /* OAKCORE_TIMECODEFUNCTIONS_H */ diff --git a/core/include/olive/core/oakcore/timerange.h b/core/include/olive/core/oakcore/timerange.h new file mode 100644 index 000000000..b12c954df --- /dev/null +++ b/core/include/olive/core/oakcore/timerange.h @@ -0,0 +1,110 @@ +/*** + + 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 OAKCORE_TIMERANGE_H +#define OAKCORE_TIMERANGE_H + +#include "export.h" +#include "rational.h" + +#ifdef __cplusplus +extern "C" { +#endif + +/** + * @file timerange.h + * @brief C ABI for the time range value type + * + * Opaque handle + free functions. All returned OakTimeRange and OakRational + * pointers are owned by the caller and must be released with + * oakcore_timerange_free() and oakcore_rational_free() respectively. + */ +typedef struct OakTimeRange OakTimeRange; + +OAKCORE_API OakTimeRange *oakcore_timerange_create(void); +OAKCORE_API OakTimeRange *oakcore_timerange_create_io(const OakRational *in, + const OakRational *out); +OAKCORE_API OakTimeRange *oakcore_timerange_copy(const OakTimeRange *self); +OAKCORE_API void oakcore_timerange_free(OakTimeRange *self); + +OAKCORE_API OakRational *oakcore_timerange_in(const OakTimeRange *self); +OAKCORE_API OakRational *oakcore_timerange_out(const OakTimeRange *self); +OAKCORE_API OakRational *oakcore_timerange_length(const OakTimeRange *self); + +OAKCORE_API void oakcore_timerange_set_in(OakTimeRange *self, + const OakRational *in); +OAKCORE_API void oakcore_timerange_set_out(OakTimeRange *self, + const OakRational *out); +OAKCORE_API void oakcore_timerange_set_range(OakTimeRange *self, + const OakRational *in, + const OakRational *out); + +OAKCORE_API int oakcore_timerange_equal(const OakTimeRange *self, + const OakTimeRange *other); + +OAKCORE_API int oakcore_timerange_overlaps_with(const OakTimeRange *self, + const OakTimeRange *other, + int in_inclusive, + int out_inclusive); +OAKCORE_API int oakcore_timerange_contains_range(const OakTimeRange *self, + const OakTimeRange *other, + int in_inclusive, + int out_inclusive); +OAKCORE_API int oakcore_timerange_contains_time(const OakTimeRange *self, + const OakRational *time); + +OAKCORE_API OakTimeRange *oakcore_timerange_combined( + const OakTimeRange *self, const OakTimeRange *other); +OAKCORE_API OakTimeRange *oakcore_timerange_combine(const OakTimeRange *a, + const OakTimeRange *b); +OAKCORE_API OakTimeRange *oakcore_timerange_intersected( + const OakTimeRange *self, const OakTimeRange *other); +OAKCORE_API OakTimeRange *oakcore_timerange_intersect(const OakTimeRange *a, + const OakTimeRange *b); + +OAKCORE_API OakTimeRange *oakcore_timerange_add(const OakTimeRange *self, + const OakRational *rhs); +OAKCORE_API OakTimeRange *oakcore_timerange_sub(const OakTimeRange *self, + const OakRational *rhs); +OAKCORE_API void oakcore_timerange_add_assign(OakTimeRange *self, + const OakRational *rhs); +OAKCORE_API void oakcore_timerange_sub_assign(OakTimeRange *self, + const OakRational *rhs); + +/** + * Splits the range into chunks of chunk_size (the first and last chunk are + * clamped to the range bounds) and writes a newly allocated owned handle per + * chunk into out_ranges. Returns the total number of chunks, so + * out_ranges == NULL (or a too-small array) can be used to query the + * required size; oakcore_timerange_split_count() is the direct equivalent + * of that query. + */ +OAKCORE_API int oakcore_timerange_split_count(const OakTimeRange *self, + int chunk_size); +OAKCORE_API int oakcore_timerange_split(const OakTimeRange *self, + int chunk_size, + OakTimeRange **out_ranges, + int out_size); + +#ifdef __cplusplus +} +#endif + +#endif /* OAKCORE_TIMERANGE_H */ diff --git a/core/include/olive/core/render/audioparams.h b/core/include/olive/core/render/audioparams.h index f4854fcd3..c98b7efd1 100644 --- a/core/include/olive/core/render/audioparams.h +++ b/core/include/olive/core/render/audioparams.h @@ -2,7 +2,7 @@ Olive - Non-Linear Video Editor Copyright (C) 2023 Olive Studios LLC - Modifications Copyright (C) 2025 mikesolar + Modifications 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 @@ -21,11 +21,12 @@ #ifndef OAK_LIBOLIVECORE_AUDIOPARAMS_H #define OAK_LIBOLIVECORE_AUDIOPARAMS_H -#include -#include +#include #include +#include "olive/core/oakcore/audioparams.h" + #include "channellayout.h" #include "sampleformat.h" #include "../util/rational.h" @@ -36,9 +37,9 @@ namespace olive::core /** * @brief Audio parameters class managing audio stream configuration * - * Channel layouts are stored as plain 64-bit masks (see channellayout.h). - * Because the mask is a simple value type, AudioParams has value semantics - * and can be copied freely. + * Consumer-side wrapper over the liboakcore C ABI: the object only holds an + * opaque OakAudioParams handle and forwards every call across the C boundary. + * The public API is unchanged from the original implementation. */ class AudioParams { public: @@ -47,12 +48,8 @@ public: * sample_rate=0, channel_layout empty, format=INVALID */ AudioParams() - : sample_rate_(0) - , channel_layout_mask_(0) - , channel_count_(0) - , format_(SampleFormat::invalid) + : handle_(oakcore_audioparams_create_invalid()) { - set_default_footage_parameters(); } /** @@ -63,31 +60,61 @@ public: */ AudioParams(const int &sample_rate, uint64_t channel_layout, const SampleFormat &format) - : sample_rate_(sample_rate) - , channel_layout_mask_(channel_layout) - , channel_count_(0) - , format_(format) + : handle_(oakcore_audioparams_create(sample_rate, channel_layout, format)) { - set_default_footage_parameters(); - timebase_ = sample_rate_as_time_base(); - calculate_channel_count(); } + + AudioParams(const AudioParams &rhs) + : handle_(oakcore_audioparams_copy(rhs.handle_)) + { + } + + AudioParams(AudioParams &&rhs) noexcept + : handle_(rhs.handle_) + { + rhs.handle_ = nullptr; + } + + ~AudioParams() + { + oakcore_audioparams_free(handle_); + } + + AudioParams &operator=(const AudioParams &rhs) + { + if (this != &rhs) { + oakcore_audioparams_free(handle_); + handle_ = oakcore_audioparams_copy(rhs.handle_); + } + return *this; + } + + AudioParams &operator=(AudioParams &&rhs) noexcept + { + if (this != &rhs) { + oakcore_audioparams_free(handle_); + handle_ = rhs.handle_; + rhs.handle_ = nullptr; + } + return *this; + } + int sample_rate() const { - return sample_rate_; + return oakcore_audioparams_sample_rate(handle_); } void set_sample_rate(int sample_rate) { - sample_rate_ = sample_rate; + oakcore_audioparams_set_sample_rate(handle_, sample_rate); } /** * @brief Channel layout as a 64-bit mask (0 if unspecified) */ - const uint64_t &channel_layout() const + uint64_t channel_layout() const { - return channel_layout_mask_; + return oakcore_audioparams_channel_layout(handle_); } /** @@ -96,122 +123,208 @@ public: */ void set_channel_layout(uint64_t mask) { - channel_layout_mask_ = mask; - calculate_channel_count(); + oakcore_audioparams_set_channel_layout(handle_, mask); } + Rational time_base() const { - return timebase_; + return Rational::from_handle(oakcore_audioparams_time_base(handle_)); } void set_time_base(const Rational &timebase) { - timebase_ = timebase; + oakcore_audioparams_set_time_base(handle_, timebase.handle()); } Rational sample_rate_as_time_base() const { - return Rational(1, sample_rate()); + return Rational::from_handle( + oakcore_audioparams_sample_rate_as_time_base(handle_)); } SampleFormat format() const { - return format_; + return SampleFormat( + static_cast(oakcore_audioparams_format(handle_))); } void set_format(SampleFormat format) { - format_ = format; + oakcore_audioparams_set_format(handle_, format); } bool enabled() const { - return enabled_; + return oakcore_audioparams_enabled(handle_) != 0; } void set_enabled(bool e) { - enabled_ = e; + oakcore_audioparams_set_enabled(handle_, e ? 1 : 0); } int stream_index() const { - return stream_index_; + return oakcore_audioparams_stream_index(handle_); } void set_stream_index(int s) { - stream_index_ = s; + oakcore_audioparams_set_stream_index(handle_, s); } int64_t duration() const { - return duration_; + return oakcore_audioparams_duration(handle_); } void set_duration(int64_t duration) { - duration_ = duration; + oakcore_audioparams_set_duration(handle_, duration); } - int64_t time_to_bytes(const double &time) const; - int64_t time_to_bytes(const Rational &time) const; - int64_t time_to_bytes_per_channel(const double &time) const; - int64_t time_to_bytes_per_channel(const Rational &time) const; - int64_t time_to_samples(const double &time) const; - int64_t time_to_samples(const Rational &time) const; - int64_t samples_to_bytes(const int64_t &samples) const; - int64_t samples_to_bytes_per_channel(const int64_t &samples) const; - Rational samples_to_time(const int64_t &samples) const; - int64_t bytes_to_samples(const int64_t &bytes) const; - Rational bytes_to_time(const int64_t &bytes) const; - Rational bytes_per_channel_to_time(const int64_t &bytes) const; - int channel_count() const; - int bytes_per_sample_per_channel() const; - int bits_per_sample() const; - bool is_valid() const; + int64_t time_to_bytes(const double &time) const + { + return oakcore_audioparams_time_to_bytes(handle_, time); + } - bool operator==(const AudioParams &other) const; - bool operator!=(const AudioParams &other) const; + int64_t time_to_bytes(const Rational &time) const + { + return oakcore_audioparams_time_to_bytes_rational(handle_, time.handle()); + } + + int64_t time_to_bytes_per_channel(const double &time) const + { + return oakcore_audioparams_time_to_bytes_per_channel(handle_, time); + } + + int64_t time_to_bytes_per_channel(const Rational &time) const + { + return oakcore_audioparams_time_to_bytes_per_channel_rational(handle_, + time.handle()); + } + + int64_t time_to_samples(const double &time) const + { + return oakcore_audioparams_time_to_samples(handle_, time); + } + + int64_t time_to_samples(const Rational &time) const + { + return oakcore_audioparams_time_to_samples_rational(handle_, time.handle()); + } + + int64_t samples_to_bytes(const int64_t &samples) const + { + return oakcore_audioparams_samples_to_bytes(handle_, samples); + } + + int64_t samples_to_bytes_per_channel(const int64_t &samples) const + { + return oakcore_audioparams_samples_to_bytes_per_channel(handle_, samples); + } + + Rational samples_to_time(const int64_t &samples) const + { + return Rational::from_handle( + oakcore_audioparams_samples_to_time(handle_, samples)); + } + + int64_t bytes_to_samples(const int64_t &bytes) const + { + return oakcore_audioparams_bytes_to_samples(handle_, bytes); + } + + Rational bytes_to_time(const int64_t &bytes) const + { + return Rational::from_handle( + oakcore_audioparams_bytes_to_time(handle_, bytes)); + } + + Rational bytes_per_channel_to_time(const int64_t &bytes) const + { + return Rational::from_handle( + oakcore_audioparams_bytes_per_channel_to_time(handle_, bytes)); + } + + int channel_count() const + { + return oakcore_audioparams_channel_count(handle_); + } + + int bytes_per_sample_per_channel() const + { + return oakcore_audioparams_bytes_per_sample_per_channel(handle_); + } + + int bits_per_sample() const + { + return oakcore_audioparams_bits_per_sample(handle_); + } + + bool is_valid() const + { + return oakcore_audioparams_is_valid(handle_) != 0; + } + + bool operator==(const AudioParams &other) const + { + return oakcore_audioparams_equals(handle_, other.handle_) != 0; + } + + bool operator!=(const AudioParams &other) const + { + return !(*this == other); + } static const std::vector k_supported_channel_layouts; static const std::vector k_supported_sample_rates; -private: - void set_default_footage_parameters() + /** + * @brief The wrapped C handle, for cross-type wrappers and direct C API use + */ + OakAudioParams *handle() const { - enabled_ = true; - stream_index_ = 0; - duration_ = 0; + return handle_; } /** - * @brief Updates channel_count_ from the current channel_layout_mask_ - * Called after any channel layout modification. + * @brief Wraps an owned C handle (takes ownership) */ - void calculate_channel_count(); + static AudioParams from_handle(OakAudioParams *handle) + { + return AudioParams(handle); + } - int sample_rate_; ///< Audio sample rate in Hz (e.g., 48000) +private: + explicit AudioParams(OakAudioParams *handle) + : handle_(handle) + { + } - /** - * @brief Channel layout mask (0 if unspecified) - * - * Plain value type mirroring FFmpeg's AV_CH_LAYOUT_* masks; no dynamic - * memory is involved, so copies are trivially safe. - */ - uint64_t channel_layout_mask_; - - int channel_count_; ///< Cached channel count from layout - - SampleFormat format_; ///< Audio sample format - - // Footage-specific parameters (serialized with footage metadata) - int enabled_; // Using int instead of bool fixes GCC 11 stringop-overflow issue (byte alignment) - int stream_index_; ///< Index in the source file's stream list - int64_t duration_; ///< Stream duration in timebase units - Rational timebase_; ///< Timebase for this audio stream + OakAudioParams *handle_; }; +inline const std::vector AudioParams::k_supported_channel_layouts = [] { + std::vector v; + const int n = oakcore_audioparams_supported_channel_layout_count(); + v.reserve(size_t(n)); + for (int i = 0; i < n; i++) { + v.push_back(oakcore_audioparams_supported_channel_layout_at(i)); + } + return v; +}(); + +inline const std::vector AudioParams::k_supported_sample_rates = [] { + std::vector v; + const int n = oakcore_audioparams_supported_sample_rate_count(); + v.reserve(size_t(n)); + for (int i = 0; i < n; i++) { + v.push_back(oakcore_audioparams_supported_sample_rate_at(i)); + } + return v; +}(); + } #endif // OAK_LIBOLIVECORE_AUDIOPARAMS_H diff --git a/core/include/olive/core/render/samplebuffer.h b/core/include/olive/core/render/samplebuffer.h index 179913d68..861eec3d7 100644 --- a/core/include/olive/core/render/samplebuffer.h +++ b/core/include/olive/core/render/samplebuffer.h @@ -2,7 +2,7 @@ Olive - Non-Linear Video Editor Copyright (C) 2023 Olive Studios LLC - Modifications Copyright (C) 2025 mikesolar + Modifications 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 @@ -22,11 +22,11 @@ #ifndef OAK_LIBOLIVECORE_SAMPLEBUFFER_H #define OAK_LIBOLIVECORE_SAMPLEBUFFER_H -#include #include -#include "audioparams.h" -#include "../util/rational.h" +#include "olive/core/oakcore/samplebuffer.h" +#include "olive/core/render/audioparams.h" +#include "olive/core/util/rational.h" namespace olive::core { @@ -34,102 +34,265 @@ namespace olive::core /** * @brief A buffer of audio samples * - * Audio samples in this structure are always stored in PLANAR (separated by channel). This is done to simplify audio - * rendering code. This replaces the old system of using QByteArrays (containing packed audio) and while SampleBuffer - * replaces many of those in the rendering/processing side of things, QByteArrays are currently still in use for - * playback, including reading to and from the cache. + * Consumer-side wrapper over the liboakcore C ABI: the object only holds an + * opaque OakSampleBuffer handle and forwards every call across the C boundary. + * The public API is unchanged from the original implementation, except that + * the getters return AudioParams/size_t by value instead of by const + * reference. + * + * Audio samples in this structure are always stored in PLANAR (separated by + * channel). This is done to simplify audio rendering code. This replaces the + * old system of using QByteArrays (containing packed audio) and while + * SampleBuffer replaces many of those in the rendering/processing side of + * things, QByteArrays are currently still in use for playback, including + * reading to and from the cache. */ class SampleBuffer { public: - SampleBuffer(); - SampleBuffer(const AudioParams &audio_params, const Rational &length); - SampleBuffer(const AudioParams &audio_params, size_t samples_per_channel); - - SampleBuffer rip_channel(int channel) const; - std::vector rip_channel_vector(int channel) const; - - const AudioParams &audio_params() const; - void set_audio_params(const AudioParams ¶ms); - - const size_t &sample_count() const + SampleBuffer() + : handle_(oakcore_samplebuffer_create()) { - return sample_count_per_channel_; } - void set_sample_count(const size_t &sample_count); + + SampleBuffer(const AudioParams &audio_params, const Rational &length) + : handle_(oakcore_samplebuffer_create_length(audio_params.handle(), + length.handle())) + { + } + + SampleBuffer(const AudioParams &audio_params, size_t samples_per_channel) + : handle_(oakcore_samplebuffer_create_samples(audio_params.handle(), + samples_per_channel)) + { + } + + SampleBuffer(const SampleBuffer &rhs) + : handle_(oakcore_samplebuffer_copy(rhs.handle_)) + { + } + + SampleBuffer(SampleBuffer &&rhs) noexcept + : handle_(rhs.handle_) + { + rhs.handle_ = nullptr; + } + + ~SampleBuffer() + { + oakcore_samplebuffer_free(handle_); + } + + SampleBuffer &operator=(const SampleBuffer &rhs) + { + if (this != &rhs) { + oakcore_samplebuffer_free(handle_); + handle_ = oakcore_samplebuffer_copy(rhs.handle_); + } + return *this; + } + + SampleBuffer &operator=(SampleBuffer &&rhs) noexcept + { + if (this != &rhs) { + oakcore_samplebuffer_free(handle_); + handle_ = rhs.handle_; + rhs.handle_ = nullptr; + } + return *this; + } + + SampleBuffer rip_channel(int channel) const + { + return from_handle(oakcore_samplebuffer_rip_channel(handle_, channel)); + } + + std::vector rip_channel_vector(int channel) const + { + const int size = oakcore_samplebuffer_rip_channel_vector( + handle_, channel, nullptr, 0); + std::vector v(static_cast(size)); + if (size > 0) { + oakcore_samplebuffer_rip_channel_vector(handle_, channel, v.data(), + size); + } + return v; + } + + AudioParams audio_params() const + { + return AudioParams::from_handle( + oakcore_samplebuffer_audio_params(handle_)); + } + + void set_audio_params(const AudioParams ¶ms) + { + oakcore_samplebuffer_set_audio_params(handle_, params.handle()); + } + + size_t sample_count() const + { + return oakcore_samplebuffer_sample_count(handle_); + } + + void set_sample_count(const size_t &sample_count) + { + oakcore_samplebuffer_set_sample_count(handle_, sample_count); + } + void set_sample_count(const Rational &length) { - set_sample_count(audio_params_.time_to_samples(length)); + oakcore_samplebuffer_set_sample_count_length(handle_, + length.handle()); } float *data(int channel) { - return data_[channel].data(); + return oakcore_samplebuffer_data(handle_, channel); } const float *data(int channel) const { - return data_.at(channel).data(); + return oakcore_samplebuffer_data(handle_, channel); } std::vector to_raw_ptrs() { - std::vector r(data_.size()); - for (size_t i = 0; i < r.size(); i++) { - r[i] = data_[i].data(); + std::vector r(static_cast(channel_count())); + if (!r.empty()) { + oakcore_samplebuffer_to_raw_ptrs(handle_, r.data()); } return r; } int channel_count() const { - return data_.size(); + return oakcore_samplebuffer_channel_count(handle_); } bool is_allocated() const { - return !data_.empty(); + return oakcore_samplebuffer_is_allocated(handle_) != 0; } - void allocate(); - void destroy(); - void reverse(); - void speed(double speed); + void allocate() + { + oakcore_samplebuffer_allocate(handle_); + } + + void destroy() + { + oakcore_samplebuffer_destroy(handle_); + } + + void reverse() + { + oakcore_samplebuffer_reverse(handle_); + } + + void speed(double speed) + { + oakcore_samplebuffer_speed(handle_, speed); + } + + void transform_volume(float f) + { + oakcore_samplebuffer_transform_volume(handle_, f); + } + + void transform_volume_for_channel(int channel, float volume) + { + oakcore_samplebuffer_transform_volume_for_channel(handle_, channel, + volume); + } - void transform_volume(float f); - void transform_volume_for_channel(int channel, float volume); static void transform_volume(float f, const SampleBuffer *input, - SampleBuffer *output); + SampleBuffer *output) + { + oakcore_samplebuffer_transform_volume_to(f, input->handle_, + output->handle_); + } + static void transform_volume_for_channel(int channel, float volume, const SampleBuffer *input, - SampleBuffer *output); + SampleBuffer *output) + { + oakcore_samplebuffer_transform_volume_for_channel_to( + channel, volume, input->handle_, output->handle_); + } + + void transform_volume_for_sample(size_t sample_index, float volume) + { + oakcore_samplebuffer_transform_volume_for_sample(handle_, sample_index, + volume); + } - void transform_volume_for_sample(size_t sample_index, float volume); void transform_volume_for_sample_on_channel(size_t sample_index, - int channel, float volume); + int channel, float volume) + { + oakcore_samplebuffer_transform_volume_for_sample_on_channel( + handle_, sample_index, channel, volume); + } - void clamp(); + void clamp() + { + oakcore_samplebuffer_clamp(handle_); + } - void silence(); - void silence(size_t start_sample, size_t end_sample); - void silence_bytes(size_t start_byte, size_t end_byte); + void silence() + { + oakcore_samplebuffer_silence(handle_); + } + + void silence(size_t start_sample, size_t end_sample) + { + oakcore_samplebuffer_silence_range(handle_, start_sample, end_sample); + } + + void silence_bytes(size_t start_byte, size_t end_byte) + { + oakcore_samplebuffer_silence_bytes(handle_, start_byte, end_byte); + } void set(int channel, const float *data, size_t sample_offset, - size_t sample_length); + size_t sample_length) + { + oakcore_samplebuffer_set(handle_, channel, data, sample_offset, + sample_length); + } + void set(int channel, const float *data, size_t sample_length) { set(channel, data, 0, sample_length); } - void fast_set(const SampleBuffer &other, int to, int from = -1); + void fast_set(const SampleBuffer &other, int to, int from = -1) + { + oakcore_samplebuffer_fast_set(handle_, other.handle_, to, from); + } + + /** + * @brief The wrapped C handle, for cross-type wrappers and direct C API use + */ + OakSampleBuffer *handle() const + { + return handle_; + } + + /** + * @brief Wraps an owned C handle (takes ownership) + */ + static SampleBuffer from_handle(OakSampleBuffer *handle) + { + return SampleBuffer(handle); + } private: - void clamp_channel(int channel); + explicit SampleBuffer(OakSampleBuffer *handle) + : handle_(handle) + { + } - AudioParams audio_params_; - - size_t sample_count_per_channel_; - - std::vector> data_; + OakSampleBuffer *handle_; }; } diff --git a/core/include/olive/core/util/bezier.h b/core/include/olive/core/util/bezier.h index 43fd07561..ddd632e61 100644 --- a/core/include/olive/core/util/bezier.h +++ b/core/include/olive/core/util/bezier.h @@ -2,7 +2,7 @@ Olive - Non-Linear Video Editor Copyright (C) 2023 Olive Studios LLC - Modifications Copyright (C) 2025 mikesolar + Modifications 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 @@ -24,113 +24,193 @@ #include +#include "olive/core/oakcore/bezier.h" + namespace olive::core { +/** + * @brief Keyframe easing bezier curve value type + * + * Consumer-side wrapper over the liboakcore C ABI: the object only holds an + * opaque OakBezier handle and forwards every call across the C boundary. + * The public API is unchanged from the original implementation. + */ class Bezier { public: - Bezier(); - Bezier(double x, double y); - Bezier(double x, double y, double cp1_x, double cp1_y, double cp2_x, - double cp2_y); + Bezier() + : handle_(oakcore_bezier_create()) + { + } - const double &x() const + Bezier(double x, double y) + : handle_(oakcore_bezier_create_xy(x, y)) { - return x_; } - const double &y() const + + Bezier(double x, double y, double cp1_x, double cp1_y, double cp2_x, + double cp2_y) + : handle_( + oakcore_bezier_create_full(x, y, cp1_x, cp1_y, cp2_x, cp2_y)) { - return y_; } - const double &cp1_x() const + + Bezier(const Bezier &rhs) + : handle_(oakcore_bezier_copy(rhs.handle_)) { - return cp1_x_; } - const double &cp1_y() const + + Bezier(Bezier &&rhs) noexcept + : handle_(rhs.handle_) { - return cp1_y_; + rhs.handle_ = nullptr; } - const double &cp2_x() const + + ~Bezier() { - return cp2_x_; + oakcore_bezier_free(handle_); } - const double &cp2_y() const + + Bezier &operator=(const Bezier &rhs) { - return cp2_y_; + if (this != &rhs) { + oakcore_bezier_free(handle_); + handle_ = oakcore_bezier_copy(rhs.handle_); + } + return *this; + } + + Bezier &operator=(Bezier &&rhs) noexcept + { + if (this != &rhs) { + oakcore_bezier_free(handle_); + handle_ = rhs.handle_; + rhs.handle_ = nullptr; + } + return *this; + } + + double x() const + { + return oakcore_bezier_x(handle_); + } + double y() const + { + return oakcore_bezier_y(handle_); + } + double cp1_x() const + { + return oakcore_bezier_cp1_x(handle_); + } + double cp1_y() const + { + return oakcore_bezier_cp1_y(handle_); + } + double cp2_x() const + { + return oakcore_bezier_cp2_x(handle_); + } + double cp2_y() const + { + return oakcore_bezier_cp2_y(handle_); } Imath::V2d to_vec() const { - return Imath::V2d(x_, y_); + return Imath::V2d(x(), y()); } Imath::V2d control_point_1_to_vec() const { - return Imath::V2d(cp1_x_, cp1_y_); + return Imath::V2d(cp1_x(), cp1_y()); } Imath::V2d control_point_2_to_vec() const { - return Imath::V2d(cp2_x_, cp2_y_); + return Imath::V2d(cp2_x(), cp2_y()); } void set_x(const double &x) { - x_ = x; + oakcore_bezier_set_x(handle_, x); } void set_y(const double &y) { - y_ = y; + oakcore_bezier_set_y(handle_, y); } void set_cp1_x(const double &cp1_x) { - cp1_x_ = cp1_x; + oakcore_bezier_set_cp1_x(handle_, cp1_x); } void set_cp1_y(const double &cp1_y) { - cp1_y_ = cp1_y; + oakcore_bezier_set_cp1_y(handle_, cp1_y); } void set_cp2_x(const double &cp2_x) { - cp2_x_ = cp2_x; + oakcore_bezier_set_cp2_x(handle_, cp2_x); } void set_cp2_y(const double &cp2_y) { - cp2_y_ = cp2_y; + oakcore_bezier_set_cp2_y(handle_, cp2_y); } - static double quadratic_xto_t(double x, double a, double b, double c); + static double quadratic_xto_t(double x, double a, double b, double c) + { + return oakcore_bezier_quadratic_xto_t(x, a, b, c); + } - static double quadratic_tto_y(double a, double b, double c, double t); + static double quadratic_tto_y(double a, double b, double c, double t) + { + return oakcore_bezier_quadratic_tto_y(a, b, c, t); + } static double quadratic_xto_y(double x, const Imath::V2d &a, - const Imath::V2d &b, const Imath::V2d &c) + const Imath::V2d &b, const Imath::V2d &c) { return quadratic_tto_y(a.y, b.y, c.y, quadratic_xto_t(x, a.x, b.x, c.x)); } - static double cubic_xto_t(double x, double a, double b, double c, double d); + static double cubic_xto_t(double x, double a, double b, double c, double d) + { + return oakcore_bezier_cubic_xto_t(x, a, b, c, d); + } - static double cubic_tto_y(double a, double b, double c, double d, double t); + static double cubic_tto_y(double a, double b, double c, double d, double t) + { + return oakcore_bezier_cubic_tto_y(a, b, c, d, t); + } static double cubic_xto_y(double x, const Imath::V2d &a, const Imath::V2d &b, - const Imath::V2d &c, const Imath::V2d &d) + const Imath::V2d &c, const Imath::V2d &d) { - return cubic_tto_y(a.y, b.y, c.y, d.y, cubic_xto_t(x, a.x, b.x, c.x, d.x)); + return cubic_tto_y(a.y, b.y, c.y, d.y, + cubic_xto_t(x, a.x, b.x, c.x, d.x)); + } + + /** + * @brief The wrapped C handle, for cross-type wrappers and direct C API use + */ + OakBezier *handle() const + { + return handle_; + } + + /** + * @brief Wraps an owned C handle (takes ownership) + */ + static Bezier from_handle(OakBezier *handle) + { + return Bezier(handle); } private: - static double calculate_t_from_x(bool cubic, double x, double a, double b, - double c, double d); + explicit Bezier(OakBezier *handle) + : handle_(handle) + { + } - double x_; - double y_; - - double cp1_x_; - double cp1_y_; - - double cp2_x_; - double cp2_y_; + OakBezier *handle_; }; } diff --git a/core/include/olive/core/util/color.h b/core/include/olive/core/util/color.h index 6101de2fe..3e08053fc 100644 --- a/core/include/olive/core/util/color.h +++ b/core/include/olive/core/util/color.h @@ -2,7 +2,7 @@ Olive - Non-Linear Video Editor Copyright (C) 2023 Olive Studios LLC - Modifications Copyright (C) 2025 mikesolar + Modifications 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 @@ -22,6 +22,7 @@ #ifndef OAK_LIBOLIVECORE_COLOR_H #define OAK_LIBOLIVECORE_COLOR_H +#include "olive/core/oakcore/color.h" #include "../render/pixelformat.h" namespace olive::core @@ -29,6 +30,10 @@ namespace olive::core /** * @brief High precision 32-bit DataType based RGBA color value + * + * Consumer-side wrapper over the liboakcore C ABI: the object only holds an + * opaque OakColor handle and forwards every call across the C boundary. + * The public API is unchanged from the original implementation. */ class Color { public: @@ -36,22 +41,55 @@ public: static constexpr unsigned int rgba = 4; Color() + : handle_(oakcore_color_create()) { - for (unsigned int i = 0; i < rgba; i++) { - data_[i] = 0.0; - } } Color(const DataType &r, const DataType &g, const DataType &b, const DataType &a = 1.0f) + : handle_(oakcore_color_create_rgba(r, g, b, a)) { - data_[0] = r; - data_[1] = g; - data_[2] = b; - data_[3] = a; } - Color(const char *data, const PixelFormat &format, int ch_layout); + Color(const char *data, const PixelFormat &format, int ch_layout) + : handle_(oakcore_color_from_data(data, format, ch_layout)) + { + } + + Color(const Color &rhs) + : handle_(oakcore_color_copy(rhs.handle_)) + { + } + + Color(Color &&rhs) noexcept + : handle_(rhs.handle_) + { + rhs.handle_ = nullptr; + } + + ~Color() + { + oakcore_color_free(handle_); + } + + Color &operator=(const Color &rhs) + { + if (this != &rhs) { + oakcore_color_free(handle_); + handle_ = oakcore_color_copy(rhs.handle_); + } + return *this; + } + + Color &operator=(Color &&rhs) noexcept + { + if (this != &rhs) { + oakcore_color_free(handle_); + handle_ = rhs.handle_; + rhs.handle_ = nullptr; + } + return *this; + } /** * @brief Creates a Color struct from hue/saturation/value @@ -59,78 +97,143 @@ public: * Hue expects a value between 0.0 and 360.0. Saturation and Value expect a value between 0.0 and 1.0. */ static Color from_hsv(const DataType &h, const DataType &s, - const DataType &v); - - const DataType &red() const + const DataType &v) { - return data_[0]; - } - const DataType &green() const - { - return data_[1]; - } - const DataType &blue() const - { - return data_[2]; - } - const DataType &alpha() const - { - return data_[3]; + return from_handle(oakcore_color_from_hsv(h, s, v)); } - void to_hsv(DataType *hue, DataType *sat, DataType *val) const; - DataType hsv_hue() const; - DataType hsv_saturation() const; - DataType value() const; + DataType red() const + { + return oakcore_color_red(handle_); + } + DataType green() const + { + return oakcore_color_green(handle_); + } + DataType blue() const + { + return oakcore_color_blue(handle_); + } + DataType alpha() const + { + return oakcore_color_alpha(handle_); + } - void to_hsl(DataType *hue, DataType *sat, DataType *lightness) const; - DataType hsl_hue() const; - DataType hsl_saturation() const; - DataType lightness() const; + void to_hsv(DataType *hue, DataType *sat, DataType *val) const + { + oakcore_color_to_hsv(handle_, hue, sat, val); + } + DataType hsv_hue() const + { + return oakcore_color_hsv_hue(handle_); + } + DataType hsv_saturation() const + { + return oakcore_color_hsv_saturation(handle_); + } + DataType value() const + { + return oakcore_color_value(handle_); + } + + void to_hsl(DataType *hue, DataType *sat, DataType *lightness) const + { + oakcore_color_to_hsl(handle_, hue, sat, lightness); + } + DataType hsl_hue() const + { + return oakcore_color_hsl_hue(handle_); + } + DataType hsl_saturation() const + { + return oakcore_color_hsl_saturation(handle_); + } + DataType lightness() const + { + return oakcore_color_lightness(handle_); + } void set_red(const DataType &red) { - data_[0] = red; + oakcore_color_set_red(handle_, red); } void set_green(const DataType &green) { - data_[1] = green; + oakcore_color_set_green(handle_, green); } void set_blue(const DataType &blue) { - data_[2] = blue; + oakcore_color_set_blue(handle_, blue); } void set_alpha(const DataType &alpha) { - data_[3] = alpha; + oakcore_color_set_alpha(handle_, alpha); } DataType *data() { - return data_; + return oakcore_color_data(handle_); } const DataType *data() const { - return data_; + return oakcore_color_const_data(handle_); } void to_data(char *out, const PixelFormat &format, - unsigned int nb_channels) const; + unsigned int nb_channels) const + { + oakcore_color_to_data(handle_, out, format, int(nb_channels)); + } static Color from_data(const char *in, const PixelFormat &format, - unsigned int nb_channels); + unsigned int nb_channels) + { + return from_handle(oakcore_color_from_data(in, format, int(nb_channels))); + } // Suuuuper rough luminance value mostly used for UI (determining whether to overlay with black // or white text) - DataType get_rough_luminance() const; + DataType get_rough_luminance() const + { + return oakcore_color_get_rough_luminance(handle_); + } // Assignment math operators - Color &operator+=(const Color &rhs); - Color &operator-=(const Color &rhs); - Color &operator+=(const DataType &rhs); - Color &operator-=(const DataType &rhs); - Color &operator*=(const DataType &rhs); - Color &operator/=(const DataType &rhs); + Color &operator+=(const Color &rhs) + { + oakcore_color_add_assign(handle_, rhs.handle_); + return *this; + } + + Color &operator-=(const Color &rhs) + { + oakcore_color_sub_assign(handle_, rhs.handle_); + return *this; + } + + Color &operator+=(const DataType &rhs) + { + oakcore_color_add_scalar_assign(handle_, rhs); + return *this; + } + + Color &operator-=(const DataType &rhs) + { + oakcore_color_sub_scalar_assign(handle_, rhs); + return *this; + } + + Color &operator*=(const DataType &rhs) + { + oakcore_color_mul_scalar_assign(handle_, rhs); + return *this; + } + + Color &operator/=(const DataType &rhs) + { + oakcore_color_div_scalar_assign(handle_, rhs); + return *this; + } // Binary math operators Color operator+(const Color &rhs) const @@ -175,8 +278,29 @@ public: return c; } + /** + * @brief The wrapped C handle, for cross-type wrappers and direct C API use + */ + OakColor *handle() const + { + return handle_; + } + + /** + * @brief Wraps an owned C handle (takes ownership) + */ + static Color from_handle(OakColor *handle) + { + return Color(handle); + } + private: - DataType data_[rgba]; + explicit Color(OakColor *handle) + : handle_(handle) + { + } + + OakColor *handle_; }; } diff --git a/core/include/olive/core/util/fractionutils.h b/core/include/olive/core/util/fractionutils.h index 77b2ece9e..df528ba85 100644 --- a/core/include/olive/core/util/fractionutils.h +++ b/core/include/olive/core/util/fractionutils.h @@ -2,7 +2,7 @@ Olive - Non-Linear Video Editor Copyright (C) 2023 Olive Studios LLC - Modifications Copyright (C) 2025 mikesolar + Modifications 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 @@ -24,14 +24,20 @@ #include +#include "olive/core/oakcore/fractionutils.h" + namespace olive::core { /** - * @brief Rounding modes for RescaleRnd() + * @brief Rounding modes for rescale_rnd() * * Mirrors the FFmpeg AVRounding modes that this codebase used before the * FFmpeg dependency was removed from core. + * + * Consumer-side wrapper over the liboakcore C ABI: every function forwards + * across the C boundary. The public API is unchanged from the original + * implementation. */ enum class FractionRounding { /** @@ -55,7 +61,10 @@ enum class FractionRounding { * * A zero denominator is preserved (with the numerator set to zero). */ -void reduce_fraction(int64_t &num, int64_t &den, int64_t max); +inline void reduce_fraction(int64_t &num, int64_t &den, int64_t max) +{ + oakcore_fractionutils_reduce_fraction(&num, &den, max); +} /** * @brief Compare two fractions @@ -64,7 +73,10 @@ void reduce_fraction(int64_t &num, int64_t &den, int64_t max); * 0 if a == b, 1 if a > b, and INT_MIN when the comparison is meaningless * (degenerate zero-denominator fractions). */ -int compare_fractions(int an, int ad, int bn, int bd); +inline int compare_fractions(int an, int ad, int bn, int bd) +{ + return oakcore_fractionutils_compare_fractions(an, ad, bn, bd); +} /** * @brief Rescale `a` by the fraction b/c: returns a * b / c @@ -73,7 +85,11 @@ int compare_fractions(int an, int ad, int bn, int bd); * product is computed with 128-bit arithmetic where available so that no * precision is lost for large timestamps. */ -int64_t rescale_rnd(int64_t a, int64_t b, int64_t c, FractionRounding rnd); +inline int64_t rescale_rnd(int64_t a, int64_t b, int64_t c, FractionRounding rnd) +{ + return oakcore_fractionutils_rescale_rnd(a, b, c, + static_cast(rnd)); +} } diff --git a/core/include/olive/core/util/rational.h b/core/include/olive/core/util/rational.h index 9b705dbae..50496db62 100644 --- a/core/include/olive/core/util/rational.h +++ b/core/include/olive/core/util/rational.h @@ -2,7 +2,7 @@ Olive - Non-Linear Video Editor Copyright (C) 2023 Olive Studios LLC - Modifications Copyright (C) 2025 mikesolar + Modifications 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 @@ -24,131 +24,295 @@ #include #include +#include #ifdef USE_OTIO #include #endif +#include "olive/core/oakcore/rational.h" + namespace olive::core { +/** + * @brief Rational number value type + * + * Consumer-side wrapper over the liboakcore C ABI: the object only holds an + * opaque OakRational handle and forwards every call across the C boundary. + * The public API is unchanged from the original implementation. + */ class Rational { public: Rational(const int &numerator = 0) + : handle_(oakcore_rational_create(numerator)) { - num_ = numerator; - den_ = 1; } Rational(const int &numerator, const int &denominator) + : handle_(oakcore_rational_create_nd(numerator, denominator)) { - num_ = numerator; - den_ = denominator; - - fix_signs(); - reduce(); } - Rational(const Rational &rhs) = default; + Rational(const Rational &rhs) + : handle_(oakcore_rational_copy(rhs.handle_)) + { + } - static Rational from_double(const double &flt, bool *ok = nullptr); - static Rational from_string(const std::string &str, bool *ok = nullptr); + Rational(Rational &&rhs) noexcept + : handle_(rhs.handle_) + { + rhs.handle_ = nullptr; + } + + ~Rational() + { + oakcore_rational_free(handle_); + } + + static Rational from_double(const double &flt, bool *ok = nullptr) + { + int c_ok = 0; + Rational r(from_handle(oakcore_rational_from_double(flt, &c_ok))); + if (ok) { + *ok = (c_ok != 0); + } + return r; + } + + static Rational from_string(const std::string &str, bool *ok = nullptr) + { + int c_ok = 0; + Rational r( + from_handle(oakcore_rational_from_string(str.c_str(), &c_ok))); + if (ok) { + *ok = (c_ok != 0); + } + return r; + } static const Rational na_n; //Assignment Operators - const Rational &operator=(const Rational &rhs); - const Rational &operator+=(const Rational &rhs); - const Rational &operator-=(const Rational &rhs); - const Rational &operator/=(const Rational &rhs); - const Rational &operator*=(const Rational &rhs); + Rational &operator=(const Rational &rhs) + { + if (this != &rhs) { + oakcore_rational_free(handle_); + handle_ = oakcore_rational_copy(rhs.handle_); + } + return *this; + } + + Rational &operator=(Rational &&rhs) noexcept + { + if (this != &rhs) { + oakcore_rational_free(handle_); + handle_ = rhs.handle_; + rhs.handle_ = nullptr; + } + return *this; + } + + Rational &operator+=(const Rational &rhs) + { + oakcore_rational_add_assign(handle_, rhs.handle_); + return *this; + } + + Rational &operator-=(const Rational &rhs) + { + oakcore_rational_sub_assign(handle_, rhs.handle_); + return *this; + } + + Rational &operator/=(const Rational &rhs) + { + oakcore_rational_div_assign(handle_, rhs.handle_); + return *this; + } + + Rational &operator*=(const Rational &rhs) + { + oakcore_rational_mul_assign(handle_, rhs.handle_); + return *this; + } //Binary math operators - Rational operator+(const Rational &rhs) const; - Rational operator-(const Rational &rhs) const; - Rational operator/(const Rational &rhs) const; - Rational operator*(const Rational &rhs) const; + Rational operator+(const Rational &rhs) const + { + Rational answer(*this); + answer += rhs; + return answer; + } + + Rational operator-(const Rational &rhs) const + { + Rational answer(*this); + answer -= rhs; + return answer; + } + + Rational operator/(const Rational &rhs) const + { + Rational answer(*this); + answer /= rhs; + return answer; + } + + Rational operator*(const Rational &rhs) const + { + Rational answer(*this); + answer *= rhs; + return answer; + } //Relational and equality operators - bool operator<(const Rational &rhs) const; - bool operator<=(const Rational &rhs) const; - bool operator>(const Rational &rhs) const; - bool operator>=(const Rational &rhs) const; - bool operator==(const Rational &rhs) const; - bool operator!=(const Rational &rhs) const; + bool operator<(const Rational &rhs) const + { + return oakcore_rational_compare(handle_, rhs.handle_) < 0; + } + + bool operator<=(const Rational &rhs) const + { + return oakcore_rational_compare(handle_, rhs.handle_) <= 0; + } + + bool operator>(const Rational &rhs) const + { + return oakcore_rational_compare(handle_, rhs.handle_) > 0; + } + + bool operator>=(const Rational &rhs) const + { + return oakcore_rational_compare(handle_, rhs.handle_) >= 0; + } + + bool operator==(const Rational &rhs) const + { + return oakcore_rational_compare(handle_, rhs.handle_) == 0; + } + + bool operator!=(const Rational &rhs) const + { + return !(*this == rhs); + } //Unary operators - const Rational &operator+() const + Rational operator+() const { return *this; } + Rational operator-() const { - return Rational(num_, -den_); + return Rational(numerator(), -denominator()); } + bool operator!() const { - return !num_; + return numerator() == 0; } //Function: convert to double - double to_double() const; + double to_double() const + { + return oakcore_rational_to_double(handle_); + } #ifdef USE_OTIO static Rational fromRationalTime(const opentime::RationalTime &t) { - // Is this the best way to do this? - return fromDouble(t.to_seconds()); + return from_double(t.to_seconds()); } - // Convert Olive rationals to opentime rationals with the given framerate (defaults to 24) - opentime::RationalTime toRationalTime(double framerate = 24) const; + opentime::RationalTime toRationalTime(double framerate = 24) const + { + // Olive can store rationals as 0/0 which causes errors in OTIO + const int den = denominator(); + opentime::RationalTime time(numerator(), den == 0 ? 1 : den); + return time.rescaled_to(framerate); + } #endif // Produce "flipped" version - Rational flipped() const; - void flip(); + Rational flipped() const + { + return from_handle(oakcore_rational_flipped(handle_)); + } + + void flip() + { + oakcore_rational_flip(handle_); + } // Returns whether the Rational is valid but equal to zero or not // // A NaN is always a null, but a null is not always a NaN bool isNull() const { - return num_ == 0; + return oakcore_rational_is_null(handle_) != 0; } // Returns whether this Rational is not a valid number (denominator == 0) bool isNaN() const { - return den_ == 0; + return oakcore_rational_is_nan(handle_) != 0; } - const int &numerator() const + int numerator() const { - return num_; - } - const int &denominator() const - { - return den_; + return oakcore_rational_numerator(handle_); } - std::string to_string() const; + int denominator() const + { + return oakcore_rational_denominator(handle_); + } + + std::string to_string() const + { + const int size = oakcore_rational_to_string(handle_, nullptr, 0); + std::string s(size_t(size) + 1, '\0'); + oakcore_rational_to_string(handle_, s.data(), size + 1); + s.resize(size_t(size)); + return s; + } friend std::ostream &operator<<(std::ostream &out, const Rational &value) { - out << value.num_ << '/' << value.den_; + out << value.numerator() << '/' << value.denominator(); return out; } -private: - void fix_signs(); - void reduce(); + /** + * @brief The wrapped C handle, for cross-type wrappers and direct C API use + */ + OakRational *handle() const + { + return handle_; + } - int num_; - int den_; + /** + * @brief Wraps an owned C handle (takes ownership) + */ + static Rational from_handle(OakRational *handle) + { + return Rational(handle); + } + +private: + explicit Rational(OakRational *handle) + : handle_(handle) + { + } + + OakRational *handle_; }; +inline const Rational Rational::na_n = + Rational::from_handle(oakcore_rational_create_nan()); + #define RATIONAL_MIN Rational(INT_MIN) #define RATIONAL_MAX Rational(INT_MAX) diff --git a/core/include/olive/core/util/stringutils.h b/core/include/olive/core/util/stringutils.h index 5277aa759..c417a6615 100644 --- a/core/include/olive/core/util/stringutils.h +++ b/core/include/olive/core/util/stringutils.h @@ -2,7 +2,7 @@ Olive - Non-Linear Video Editor Copyright (C) 2023 Olive Studios LLC - Modifications Copyright (C) 2025 mikesolar + Modifications 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 @@ -23,13 +23,24 @@ #define OAK_LIBOLIVECORE_STRINGUTILS_H #include +#include #include #include #include +#include "olive/core/oakcore/stringutils.h" + namespace olive::core { +/** + * @brief String utility functions + * + * Consumer-side wrapper over the liboakcore C ABI: every non-inlined call + * is forwarded across the C boundary. The public API is unchanged from the + * original implementation. The class has static members only, so there is + * no opaque handle. + */ class StringUtils { public: /** @@ -47,7 +58,20 @@ public: * * A vector of strings split by the specified delimiter. */ - static std::vector split(const std::string &s, char separator); + static std::vector split(const std::string &s, char separator) + { + int count = 0; + char **arr = oakcore_stringutils_split(s.c_str(), separator, &count); + std::vector output; + if (arr) { + output.reserve(size_t(count)); + for (int i = 0; i < count; i++) { + output.emplace_back(arr[i]); + } + oakcore_stringutils_free_string_array(arr, count); + } + return output; + } /** * @brief Splits a string into a list of strings using regular expressions. @@ -65,10 +89,26 @@ public: * A vector of strings split wherever the regular expression matched. */ static std::vector split_regex(const std::string &s, - const std::regex ®ex); + const std::regex ®ex) + { + // std::regex cannot cross the C ABI, so this overload is implemented + // inline here with only standard library facilities, exactly like the + // original. The C ABI exposes the same functionality as + // oakcore_stringutils_split_regex() taking a pattern string. + std::vector output; + + std::sregex_token_iterator iter(s.begin(), s.end(), regex, -1); + std::sregex_token_iterator end; + for (; iter != end; iter++) { + output.push_back(*iter); + } + + return output; + } /** - * @brief Convert a string to int using a bool pointer to determine success rather than an exception + * @brief Convert a string to int using a bool pointer to determine + * success rather than an exception * * @param s * @@ -86,7 +126,15 @@ public: * * Either the int parsed from the string, or 0 (with *ok set to false) on parser error. */ - static int to_int(const std::string &s, int base, bool *ok = nullptr); + static int to_int(const std::string &s, int base, bool *ok = nullptr) + { + int c_ok = 0; + const int x = oakcore_stringutils_to_int(s.c_str(), base, &c_ok); + if (ok) { + *ok = (c_ok != 0); + } + return x; + } /** * @brief Overloaded function @@ -157,7 +205,28 @@ public: * * A formatted string in std::string form. */ - static std::string format(const char *fmt, ...); + static std::string format(const char *fmt, ...) + { + va_list ap1, ap2; + va_start(ap1, fmt); + + // Need to duplicate because the va_list is consumed by each C API call + va_copy(ap2, ap1); + + const int size = oakcore_stringutils_format_v(nullptr, 0, fmt, ap1); + + // Create string with size, adding 1 for the null terminator + std::string r(size_t(size) + 1, '\0'); + oakcore_stringutils_format_v(r.data(), size + 1, fmt, ap2); + + va_end(ap2); + va_end(ap1); + + // Pop null terminator + r.pop_back(); + + return r; + } // trim from start (in place) static inline void ltrim(std::string &s) diff --git a/core/include/olive/core/util/timecodefunctions.h b/core/include/olive/core/util/timecodefunctions.h index 1f532665a..e4e86c8f2 100644 --- a/core/include/olive/core/util/timecodefunctions.h +++ b/core/include/olive/core/util/timecodefunctions.h @@ -2,7 +2,7 @@ Olive - Non-Linear Video Editor Copyright (C) 2023 Olive Studios LLC - Modifications Copyright (C) 2025 mikesolar + Modifications 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 @@ -25,6 +25,9 @@ #include "rational.h" #include +#include + +#include "olive/core/oakcore/timecodefunctions.h" namespace olive::core { @@ -32,11 +35,17 @@ namespace olive::core /** * @brief Functions for converting times/timecodes/timestamps * + * Consumer-side wrapper over the liboakcore C ABI: the class is all-static + * and every call is forwarded across the C boundary (times and timebases + * cross it as borrowed Rational handles). The public API is unchanged from + * the original implementation. + * * Olive uses the following terminology through its code: * * `time` - time in seconds presented in a Rational form * `timebase` - the base time unit of an audio/video stream in seconds - * `timestamp` - an integer representation of a time in timebase units (in many cases is used like a frame number) + * `timestamp` - an integer representation of a time in timebase units + * (in many cases is used like a frame number) * `timecode` a user-friendly string representation of a time according to Timecode::Display */ class Timecode { @@ -52,40 +61,104 @@ public: enum Rounding { k_ceil, k_floor, k_round }; /** - * @brief Convert a timestamp (according to a Rational timebase) to a user-friendly string representation + * @brief Convert a timestamp (according to a Rational timebase) to a + * user-friendly string representation */ static std::string time_to_timecode(const Rational &time, const Rational &timebase, const Display &display, - bool show_plus_if_positive = false); + bool show_plus_if_positive = false) + { + const OakTimecodeDisplay c_display = + static_cast(display); + const int c_show_plus = show_plus_if_positive ? 1 : 0; + const int size = oakcore_timecode_time_to_timecode( + time.handle(), timebase.handle(), c_display, c_show_plus, nullptr, + 0); + std::string s(size_t(size) + 1, '\0'); + oakcore_timecode_time_to_timecode(time.handle(), timebase.handle(), + c_display, c_show_plus, s.data(), + size + 1); + s.resize(size_t(size)); + return s; + } + static Rational timecode_to_time(std::string timecode, const Rational &timebase, const Display &display, - bool *ok = nullptr); + bool *ok = nullptr) + { + int c_ok = 0; + Rational r = Rational::from_handle(oakcore_timecode_timecode_to_time( + timecode.c_str(), timebase.handle(), + static_cast(display), &c_ok)); + if (ok) { + *ok = (c_ok != 0); + } + return r; + } - static std::string time_to_string(int64_t ms); + static std::string time_to_string(int64_t ms) + { + const int size = oakcore_timecode_time_to_string(ms, nullptr, 0); + std::string s(size_t(size) + 1, '\0'); + oakcore_timecode_time_to_string(ms, s.data(), size + 1); + s.resize(size_t(size)); + return s; + } static Rational snap_time_to_timebase(const Rational &time, const Rational &timebase, - Rounding floor = k_round); + Rounding floor = k_round) + { + return Rational::from_handle(oakcore_timecode_snap_time_to_timebase( + time.handle(), timebase.handle(), + static_cast(floor))); + } static int64_t time_to_timestamp(const Rational &time, const Rational &timebase, - Rounding floor = k_round); + Rounding floor = k_round) + { + return oakcore_timecode_time_to_timestamp( + time.handle(), timebase.handle(), + static_cast(floor)); + } + static int64_t time_to_timestamp(const double &time, const Rational &timebase, - Rounding floor = k_round); + Rounding floor = k_round) + { + return oakcore_timecode_time_to_timestamp_d( + time, timebase.handle(), static_cast(floor)); + } static int64_t rescale_timestamp(const int64_t &ts, const Rational &source, - const Rational &dest); + const Rational &dest) + { + return oakcore_timecode_rescale_timestamp(ts, source.handle(), + dest.handle()); + } + static int64_t rescale_timestamp_ceil(const int64_t &ts, const Rational &source, - const Rational &dest); + const Rational &dest) + { + return oakcore_timecode_rescale_timestamp_ceil(ts, source.handle(), + dest.handle()); + } static Rational timestamp_to_time(const int64_t ×tamp, - const Rational &timebase); + const Rational &timebase) + { + return Rational::from_handle(oakcore_timecode_timestamp_to_time( + timestamp, timebase.handle())); + } - static bool timebase_is_drop_frame(const Rational &timebase); + static bool timebase_is_drop_frame(const Rational &timebase) + { + return oakcore_timecode_timebase_is_drop_frame(timebase.handle()) != 0; + } }; } diff --git a/core/include/olive/core/util/timerange.h b/core/include/olive/core/util/timerange.h index 68cbd87bc..8d8297557 100644 --- a/core/include/olive/core/util/timerange.h +++ b/core/include/olive/core/util/timerange.h @@ -2,7 +2,7 @@ Olive - Non-Linear Video Editor Copyright (C) 2023 Olive Studios LLC - Modifications Copyright (C) 2025 mikesolar + Modifications 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 @@ -22,67 +22,223 @@ #ifndef OAK_LIBOLIVECORE_TIMERANGE_H #define OAK_LIBOLIVECORE_TIMERANGE_H +#include +#include +#include #include #include +#include "olive/core/oakcore/timerange.h" #include "rational.h" +#include "timecodefunctions.h" namespace olive::core { +/** + * @brief A range of time, normalized so that in() <= out() + * + * Consumer-side wrapper over the liboakcore C ABI: the object only holds an + * opaque OakTimeRange handle and forwards every call across the C boundary. + * The public API is unchanged from the original implementation, except that + * the getters return Rational by value instead of by const reference. + */ class TimeRange { public: - TimeRange() = default; - TimeRange(const Rational &in, const Rational &out); - TimeRange(const TimeRange &r) - : TimeRange(r.in(), r.out()) + TimeRange() + : handle_(oakcore_timerange_create()) { } + TimeRange(const Rational &in, const Rational &out) + : handle_(oakcore_timerange_create_io(in.handle(), out.handle())) + { + } + + TimeRange(const TimeRange &r) + : handle_(oakcore_timerange_copy(r.handle_)) + { + } + + TimeRange(TimeRange &&r) noexcept + : handle_(r.handle_) + { + r.handle_ = nullptr; + } + + ~TimeRange() + { + oakcore_timerange_free(handle_); + } + TimeRange &operator=(const TimeRange &r) { - set_range(r.in(), r.out()); + if (this != &r) { + oakcore_timerange_free(handle_); + handle_ = oakcore_timerange_copy(r.handle_); + } return *this; } - const Rational &in() const; - const Rational &out() const; - const Rational &length() const; + TimeRange &operator=(TimeRange &&r) noexcept + { + if (this != &r) { + oakcore_timerange_free(handle_); + handle_ = r.handle_; + r.handle_ = nullptr; + } + return *this; + } - void set_in(const Rational &in); - void set_out(const Rational &out); - void set_range(const Rational &in, const Rational &out); + Rational in() const + { + return Rational::from_handle(oakcore_timerange_in(handle_)); + } - bool operator==(const TimeRange &r) const; - bool operator!=(const TimeRange &r) const; + Rational out() const + { + return Rational::from_handle(oakcore_timerange_out(handle_)); + } + + Rational length() const + { + return Rational::from_handle(oakcore_timerange_length(handle_)); + } + + void set_in(const Rational &in) + { + oakcore_timerange_set_in(handle_, in.handle()); + } + + void set_out(const Rational &out) + { + oakcore_timerange_set_out(handle_, out.handle()); + } + + void set_range(const Rational &in, const Rational &out) + { + oakcore_timerange_set_range(handle_, in.handle(), out.handle()); + } + + bool operator==(const TimeRange &r) const + { + return oakcore_timerange_equal(handle_, r.handle_) != 0; + } + + bool operator!=(const TimeRange &r) const + { + return !(*this == r); + } bool overlaps_with(const TimeRange &a, bool in_inclusive = true, - bool out_inclusive = true) const; + bool out_inclusive = true) const + { + return oakcore_timerange_overlaps_with(handle_, a.handle_, + in_inclusive ? 1 : 0, + out_inclusive ? 1 : 0) != 0; + } + bool contains(const TimeRange &a, bool in_inclusive = true, - bool out_inclusive = true) const; - bool contains(const Rational &r) const; + bool out_inclusive = true) const + { + return oakcore_timerange_contains_range(handle_, a.handle_, + in_inclusive ? 1 : 0, + out_inclusive ? 1 : 0) != 0; + } - TimeRange combined(const TimeRange &a) const; - static TimeRange combine(const TimeRange &a, const TimeRange &b); - TimeRange intersected(const TimeRange &a) const; - static TimeRange intersect(const TimeRange &a, const TimeRange &b); + bool contains(const Rational &r) const + { + return oakcore_timerange_contains_time(handle_, r.handle()) != 0; + } - TimeRange operator+(const Rational &rhs) const; - TimeRange operator-(const Rational &rhs) const; + TimeRange combined(const TimeRange &a) const + { + return from_handle(oakcore_timerange_combined(handle_, a.handle_)); + } - const TimeRange &operator+=(const Rational &rhs); - const TimeRange &operator-=(const Rational &rhs); + static TimeRange combine(const TimeRange &a, const TimeRange &b) + { + return from_handle(oakcore_timerange_combine(a.handle_, b.handle_)); + } - std::list split(const int &chunk_size) const; + TimeRange intersected(const TimeRange &a) const + { + return from_handle(oakcore_timerange_intersected(handle_, a.handle_)); + } + + static TimeRange intersect(const TimeRange &a, const TimeRange &b) + { + return from_handle(oakcore_timerange_intersect(a.handle_, b.handle_)); + } + + TimeRange operator+(const Rational &rhs) const + { + return from_handle(oakcore_timerange_add(handle_, rhs.handle())); + } + + TimeRange operator-(const Rational &rhs) const + { + return from_handle(oakcore_timerange_sub(handle_, rhs.handle())); + } + + const TimeRange &operator+=(const Rational &rhs) + { + oakcore_timerange_add_assign(handle_, rhs.handle()); + return *this; + } + + const TimeRange &operator-=(const Rational &rhs) + { + oakcore_timerange_sub_assign(handle_, rhs.handle()); + return *this; + } + + std::list split(const int &chunk_size) const + { + const int count = oakcore_timerange_split_count(handle_, chunk_size); + std::vector handles{size_t(count)}; + oakcore_timerange_split(handle_, chunk_size, handles.data(), count); + + std::list ranges; + for (OakTimeRange *h : handles) { + ranges.push_back(from_handle(h)); + } + return ranges; + } + + /** + * @brief The wrapped C handle, for cross-type wrappers and direct C API use + */ + OakTimeRange *handle() const + { + return handle_; + } + + /** + * @brief Wraps an owned C handle (takes ownership) + */ + static TimeRange from_handle(OakTimeRange *handle) + { + return TimeRange(handle); + } private: - void normalize(); + explicit TimeRange(OakTimeRange *handle) + : handle_(handle) + { + } - Rational in_; - Rational out_; - Rational length_; + OakTimeRange *handle_; }; +/** + * @brief A list of TimeRanges, kept merged and non-overlapping on insert() + * + * Consumer-side inline reimplementation over the wrapped TimeRange: every + * operation is expressed through the public API of olive::core::TimeRange + * and olive::core::Rational, so a container like this needs no C ABI surface + * of its own. The public API is unchanged from the original implementation. + */ class TimeRangeList { public: TimeRangeList() = default; @@ -92,11 +248,46 @@ public: { } - void insert(const TimeRangeList &list_to_add); - void insert(TimeRange range_to_add); + void insert(const TimeRangeList &list_to_add) + { + for (auto it = list_to_add.cbegin(); it != list_to_add.cend(); it++) { + insert(*it); + } + } - void remove(const TimeRange &remove); - void remove(const TimeRangeList &list); + void insert(TimeRange range_to_add) + { + // See if list contains this range + if (contains(range_to_add)) { + return; + } + + // Does not contain range, so we'll almost certainly be adding it in some way + for (auto it = array_.begin(); it != array_.end();) { + const TimeRange &compare = *it; + + if (compare.overlaps_with(range_to_add)) { + range_to_add = TimeRange::combine(range_to_add, compare); + it = array_.erase(it); + } else { + it++; + } + } + + array_.push_back(range_to_add); + } + + void remove(const TimeRange &remove) + { + util_remove(&array_, remove); + } + + void remove(const TimeRangeList &list) + { + for (const TimeRange &r : list) { + remove(r); + } + } template static void util_remove(std::vector *list, const TimeRange &remove) @@ -138,7 +329,16 @@ public: } bool contains(const TimeRange &range, bool in_inclusive = true, - bool out_inclusive = true) const; + bool out_inclusive = true) const + { + for (int i = 0; i < size(); i++) { + if (array_.at(i).contains(range, in_inclusive, out_inclusive)) { + return true; + } + } + + return false; + } bool contains(const Rational &r) const { @@ -178,13 +378,62 @@ public: return array_.size(); } - void shift(const Rational &diff); + void shift(const Rational &diff) + { + for (int i = 0; i < array_.size(); i++) { + array_[i] += diff; + } + } - void trim_in(const Rational &diff); + void trim_in(const Rational &diff) + { + // Re-do list since we want to handle overlaps + TimeRangeList temp = *this; - void trim_out(const Rational &diff); + clear(); - TimeRangeList intersects(const TimeRange &range) const; + for (auto it = temp.array_.begin(); it != temp.array_.end(); it++) { + TimeRange &r = *it; + r.set_in(r.in() + diff); + insert(r); + } + } + + void trim_out(const Rational &diff) + { + // Re-do list since we want to handle overlaps + TimeRangeList temp = *this; + + clear(); + + for (auto it = temp.array_.begin(); it != temp.array_.end(); it++) { + TimeRange &r = *it; + r.set_out(r.out() + diff); + insert(r); + } + } + + TimeRangeList intersects(const TimeRange &range) const + { + TimeRangeList intersect_list; + + for (int i = 0; i < size(); i++) { + const TimeRange &compare = array_.at(i); + + if (compare.out() <= range.in() || compare.in() >= range.out()) { + // No intersect + continue; + } else { + // Crop the time range to the range and add it to the list + TimeRange cropped(std::max(range.in(), compare.in()), + std::min(range.out(), compare.out())); + + intersect_list.insert(cropped); + } + } + + return intersect_list; + } using const_iterator = std::vector::const_iterator; @@ -237,17 +486,70 @@ private: std::vector array_; }; +/** + * @brief Steps through a TimeRangeList frame by frame at a given timebase + * + * Consumer-side inline reimplementation: frame snapping and timestamp + * conversion go through the wrapped olive::core::Timecode functions, the + * range storage through the wrapped olive::core::TimeRangeList above. The + * public API is unchanged from the original implementation. + */ class TimeRangeListFrameIterator { public: - TimeRangeListFrameIterator(); + TimeRangeListFrameIterator() + : TimeRangeListFrameIterator(TimeRangeList(), Rational::na_n) + { + } + TimeRangeListFrameIterator(const TimeRangeList &list, - const Rational &timebase); + const Rational &timebase) + : list_(list) + , timebase_(timebase) + , range_index_(-1) + , size_(-1) + , frame_index_(0) + , custom_range_(false) + { + if (!list_.isEmpty() && timebase_.isNull()) { + std::cerr + << "TimeRangeListFrameIterator created with null timebase but " + "non-empty list, this will likely lead to infinite loops" + << std::endl; + } - Rational snap(const Rational &r) const; + update_index_if_necessary(); + } - bool get_next(Rational *out); + Rational snap(const Rational &r) const + { + return Timecode::snap_time_to_timebase(r, timebase_, Timecode::k_floor); + } - bool has_next() const; + bool get_next(Rational *out) + { + if (!has_next()) { + return false; + } + + // Output current value + *out = current_; + + // Determine next value by adding timebase + current_ += timebase_; + + // If this time is outside the current range, jump to the next one + update_index_if_necessary(); + + // Increment frame index + frame_index_++; + + return true; + } + + bool has_next() const + { + return range_index_ < list_.size(); + } std::vector to_vector() const { @@ -260,7 +562,31 @@ public: return times; } - int size(); + int size() + { + if (size_ == -1) { + // Size isn't calculated automatically for optimization, so we'll calculate it now + size_ = 0; + + for (const TimeRange &range : list_) { + Rational start = snap(range.in()); + Rational end = Timecode::snap_time_to_timebase( + range.out(), timebase_, Timecode::k_floor); + + if (end == range.out()) { + end -= timebase_; + } + + int64_t start_ts = + Timecode::time_to_timestamp(start, timebase_); + int64_t end_ts = Timecode::time_to_timestamp(end, timebase_); + + size_ += 1 + (end_ts - start_ts); + } + } + + return size_; + } void reset() { @@ -293,7 +619,18 @@ public: } private: - void update_index_if_necessary(); + void update_index_if_necessary() + { + while (range_index_ < list_.size() && + (range_index_ == -1 || + current_ >= list_.at(range_index_).out())) { + range_index_++; + + if (range_index_ < list_.size()) { + current_ = snap(list_.at(range_index_).in()); + } + } + } TimeRangeList list_; diff --git a/core/src/capi/audioparams.cpp b/core/src/capi/audioparams.cpp new file mode 100644 index 000000000..660bed08d --- /dev/null +++ b/core/src/capi/audioparams.cpp @@ -0,0 +1,315 @@ +/*** + + 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 "oakcore/audioparams.h" + +#include "render/audioparams.h" + +namespace +{ + +olive::core::internal::AudioParams *impl(OakAudioParams *h) +{ + return reinterpret_cast(h); +} + +const olive::core::internal::AudioParams *impl(const OakAudioParams *h) +{ + return reinterpret_cast(h); +} + +OakAudioParams *wrap(olive::core::internal::AudioParams *p) +{ + return reinterpret_cast(p); +} + +const olive::core::internal::Rational *impl(const OakRational *h) +{ + return reinterpret_cast(h); +} + +OakRational *wrap(olive::core::internal::Rational *r) +{ + return reinterpret_cast(r); +} + +olive::core::SampleFormat to_format(int f) +{ + return olive::core::SampleFormat( + static_cast(f)); +} + +// The supported channel layouts / sample rates as constant data. These +// mirror AudioParams::k_supported_channel_layouts / +// k_supported_sample_rates (src/render/audioparams.cpp) and must be kept in +// sync with them. The C API serves these instead of reading the +// implementation's std::vector statics directly so that the functions are +// safe to call during a consumer's static initialization even when +// liboakcore is linked statically (cross-TU static init order is +// unspecified; the implementation's vectors may not be constructed yet). +constexpr uint64_t k_supported_channel_layouts[] = { + olive::core::k_channel_layout_mono, + olive::core::k_channel_layout_stereo, + olive::core::k_channel_layout2_1, + olive::core::k_channel_layout5_point1, + olive::core::k_channel_layout7_point1, +}; + +constexpr int k_supported_sample_rates[] = { + 8000, 11025, 16000, 22050, 24000, 32000, 44100, 48000, 88200, 96000, +}; + +template +T at_or_zero(const T (&arr)[N], int index) +{ + if (index < 0 || size_t(index) >= N) { + return 0; + } + return arr[index]; +} + +} // namespace + +extern "C" +{ + +OakAudioParams *oakcore_audioparams_create(int sample_rate, + uint64_t channel_layout, int format) +{ + return wrap(new olive::core::internal::AudioParams( + sample_rate, channel_layout, to_format(format))); +} + +OakAudioParams *oakcore_audioparams_create_invalid(void) +{ + return wrap(new olive::core::internal::AudioParams()); +} + +OakAudioParams *oakcore_audioparams_copy(const OakAudioParams *self) +{ + return wrap(new olive::core::internal::AudioParams(*impl(self))); +} + +void oakcore_audioparams_free(OakAudioParams *self) +{ + delete impl(self); +} + +int oakcore_audioparams_sample_rate(const OakAudioParams *self) +{ + return impl(self)->sample_rate(); +} + +void oakcore_audioparams_set_sample_rate(OakAudioParams *self, int sample_rate) +{ + impl(self)->set_sample_rate(sample_rate); +} + +uint64_t oakcore_audioparams_channel_layout(const OakAudioParams *self) +{ + return impl(self)->channel_layout(); +} + +void oakcore_audioparams_set_channel_layout(OakAudioParams *self, uint64_t mask) +{ + impl(self)->set_channel_layout(mask); +} + +OakRational *oakcore_audioparams_time_base(const OakAudioParams *self) +{ + return wrap(new olive::core::internal::Rational(impl(self)->time_base())); +} + +void oakcore_audioparams_set_time_base(OakAudioParams *self, + const OakRational *timebase) +{ + impl(self)->set_time_base(*impl(timebase)); +} + +OakRational *oakcore_audioparams_sample_rate_as_time_base( + const OakAudioParams *self) +{ + return wrap( + new olive::core::internal::Rational(impl(self)->sample_rate_as_time_base())); +} + +int oakcore_audioparams_format(const OakAudioParams *self) +{ + return int(impl(self)->format()); +} + +void oakcore_audioparams_set_format(OakAudioParams *self, int format) +{ + impl(self)->set_format(to_format(format)); +} + +int oakcore_audioparams_enabled(const OakAudioParams *self) +{ + return impl(self)->enabled() ? 1 : 0; +} + +void oakcore_audioparams_set_enabled(OakAudioParams *self, int enabled) +{ + impl(self)->set_enabled(enabled != 0); +} + +int oakcore_audioparams_stream_index(const OakAudioParams *self) +{ + return impl(self)->stream_index(); +} + +void oakcore_audioparams_set_stream_index(OakAudioParams *self, int stream_index) +{ + impl(self)->set_stream_index(stream_index); +} + +int64_t oakcore_audioparams_duration(const OakAudioParams *self) +{ + return impl(self)->duration(); +} + +void oakcore_audioparams_set_duration(OakAudioParams *self, int64_t duration) +{ + impl(self)->set_duration(duration); +} + +int64_t oakcore_audioparams_time_to_bytes(const OakAudioParams *self, double time) +{ + return impl(self)->time_to_bytes(time); +} + +int64_t oakcore_audioparams_time_to_bytes_rational(const OakAudioParams *self, + const OakRational *time) +{ + return impl(self)->time_to_bytes(*impl(time)); +} + +int64_t oakcore_audioparams_time_to_bytes_per_channel(const OakAudioParams *self, + double time) +{ + return impl(self)->time_to_bytes_per_channel(time); +} + +int64_t oakcore_audioparams_time_to_bytes_per_channel_rational( + const OakAudioParams *self, const OakRational *time) +{ + return impl(self)->time_to_bytes_per_channel(*impl(time)); +} + +int64_t oakcore_audioparams_time_to_samples(const OakAudioParams *self, + double time) +{ + return impl(self)->time_to_samples(time); +} + +int64_t oakcore_audioparams_time_to_samples_rational(const OakAudioParams *self, + const OakRational *time) +{ + return impl(self)->time_to_samples(*impl(time)); +} + +int64_t oakcore_audioparams_samples_to_bytes(const OakAudioParams *self, + int64_t samples) +{ + return impl(self)->samples_to_bytes(samples); +} + +int64_t oakcore_audioparams_samples_to_bytes_per_channel( + const OakAudioParams *self, int64_t samples) +{ + return impl(self)->samples_to_bytes_per_channel(samples); +} + +OakRational *oakcore_audioparams_samples_to_time(const OakAudioParams *self, + int64_t samples) +{ + return wrap( + new olive::core::internal::Rational(impl(self)->samples_to_time(samples))); +} + +int64_t oakcore_audioparams_bytes_to_samples(const OakAudioParams *self, + int64_t bytes) +{ + return impl(self)->bytes_to_samples(bytes); +} + +OakRational *oakcore_audioparams_bytes_to_time(const OakAudioParams *self, + int64_t bytes) +{ + return wrap( + new olive::core::internal::Rational(impl(self)->bytes_to_time(bytes))); +} + +OakRational *oakcore_audioparams_bytes_per_channel_to_time( + const OakAudioParams *self, int64_t bytes) +{ + return wrap(new olive::core::internal::Rational( + impl(self)->bytes_per_channel_to_time(bytes))); +} + +int oakcore_audioparams_channel_count(const OakAudioParams *self) +{ + return impl(self)->channel_count(); +} + +int oakcore_audioparams_bytes_per_sample_per_channel(const OakAudioParams *self) +{ + return impl(self)->bytes_per_sample_per_channel(); +} + +int oakcore_audioparams_bits_per_sample(const OakAudioParams *self) +{ + return impl(self)->bits_per_sample(); +} + +int oakcore_audioparams_is_valid(const OakAudioParams *self) +{ + return impl(self)->is_valid() ? 1 : 0; +} + +int oakcore_audioparams_equals(const OakAudioParams *self, + const OakAudioParams *other) +{ + return (*impl(self) == *impl(other)) ? 1 : 0; +} + +int oakcore_audioparams_supported_channel_layout_count(void) +{ + return int(sizeof(k_supported_channel_layouts) / + sizeof(k_supported_channel_layouts[0])); +} + +uint64_t oakcore_audioparams_supported_channel_layout_at(int index) +{ + return at_or_zero(k_supported_channel_layouts, index); +} + +int oakcore_audioparams_supported_sample_rate_count(void) +{ + return int(sizeof(k_supported_sample_rates) / + sizeof(k_supported_sample_rates[0])); +} + +int oakcore_audioparams_supported_sample_rate_at(int index) +{ + return at_or_zero(k_supported_sample_rates, index); +} + +} // extern "C" diff --git a/core/src/capi/bezier.cpp b/core/src/capi/bezier.cpp new file mode 100644 index 000000000..c04b53a8d --- /dev/null +++ b/core/src/capi/bezier.cpp @@ -0,0 +1,157 @@ +/*** + + 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 "oakcore/bezier.h" + +#include "util/bezier.h" + +namespace +{ + +olive::core::internal::Bezier *impl(OakBezier *h) +{ + return reinterpret_cast(h); +} + +const olive::core::internal::Bezier *impl(const OakBezier *h) +{ + return reinterpret_cast(h); +} + +OakBezier *wrap(olive::core::internal::Bezier *b) +{ + return reinterpret_cast(b); +} + +} // namespace + +extern "C" +{ + +OakBezier *oakcore_bezier_create(void) +{ + return wrap(new olive::core::internal::Bezier()); +} + +OakBezier *oakcore_bezier_create_xy(double x, double y) +{ + return wrap(new olive::core::internal::Bezier(x, y)); +} + +OakBezier *oakcore_bezier_create_full(double x, double y, double cp1_x, + double cp1_y, double cp2_x, double cp2_y) +{ + return wrap(new olive::core::internal::Bezier(x, y, cp1_x, cp1_y, cp2_x, + cp2_y)); +} + +OakBezier *oakcore_bezier_copy(const OakBezier *self) +{ + return wrap(new olive::core::internal::Bezier(*impl(self))); +} + +void oakcore_bezier_free(OakBezier *self) +{ + delete impl(self); +} + +double oakcore_bezier_x(const OakBezier *self) +{ + return impl(self)->x(); +} + +double oakcore_bezier_y(const OakBezier *self) +{ + return impl(self)->y(); +} + +double oakcore_bezier_cp1_x(const OakBezier *self) +{ + return impl(self)->cp1_x(); +} + +double oakcore_bezier_cp1_y(const OakBezier *self) +{ + return impl(self)->cp1_y(); +} + +double oakcore_bezier_cp2_x(const OakBezier *self) +{ + return impl(self)->cp2_x(); +} + +double oakcore_bezier_cp2_y(const OakBezier *self) +{ + return impl(self)->cp2_y(); +} + +void oakcore_bezier_set_x(OakBezier *self, double x) +{ + impl(self)->set_x(x); +} + +void oakcore_bezier_set_y(OakBezier *self, double y) +{ + impl(self)->set_y(y); +} + +void oakcore_bezier_set_cp1_x(OakBezier *self, double cp1_x) +{ + impl(self)->set_cp1_x(cp1_x); +} + +void oakcore_bezier_set_cp1_y(OakBezier *self, double cp1_y) +{ + impl(self)->set_cp1_y(cp1_y); +} + +void oakcore_bezier_set_cp2_x(OakBezier *self, double cp2_x) +{ + impl(self)->set_cp2_x(cp2_x); +} + +void oakcore_bezier_set_cp2_y(OakBezier *self, double cp2_y) +{ + impl(self)->set_cp2_y(cp2_y); +} + +double oakcore_bezier_quadratic_xto_t(double x, double a, double b, double c) +{ + return olive::core::internal::Bezier::quadratic_xto_t(x, a, b, c); +} + +double oakcore_bezier_quadratic_tto_y(double a, double b, double c, double t) +{ + return olive::core::internal::Bezier::quadratic_tto_y(a, b, c, t); +} + +double oakcore_bezier_cubic_xto_t(double x, double a, double b, double c, + double d) +{ + return olive::core::internal::Bezier::cubic_xto_t(x, a, b, c, d); +} + +double oakcore_bezier_cubic_tto_y(double a, double b, double c, double d, + double t) +{ + return olive::core::internal::Bezier::cubic_tto_y(a, b, c, d, t); +} + +} // extern "C" diff --git a/core/src/capi/color.cpp b/core/src/capi/color.cpp new file mode 100644 index 000000000..1c8d6af01 --- /dev/null +++ b/core/src/capi/color.cpp @@ -0,0 +1,220 @@ +/*** + + 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 "oakcore/color.h" + +#include "util/color.h" + +namespace +{ + +olive::core::internal::Color *impl(OakColor *h) +{ + return reinterpret_cast(h); +} + +const olive::core::internal::Color *impl(const OakColor *h) +{ + return reinterpret_cast(h); +} + +OakColor *wrap(olive::core::internal::Color *c) +{ + return reinterpret_cast(c); +} + +olive::core::PixelFormat to_format(int f) +{ + return olive::core::PixelFormat( + static_cast(f)); +} + +} // namespace + +extern "C" +{ + +OakColor *oakcore_color_create(void) +{ + return wrap(new olive::core::internal::Color()); +} + +OakColor *oakcore_color_create_rgba(float r, float g, float b, float a) +{ + return wrap(new olive::core::internal::Color(r, g, b, a)); +} + +OakColor *oakcore_color_copy(const OakColor *self) +{ + return wrap(new olive::core::internal::Color(*impl(self))); +} + +void oakcore_color_free(OakColor *self) +{ + delete impl(self); +} + +OakColor *oakcore_color_from_hsv(float h, float s, float v) +{ + return wrap( + new olive::core::internal::Color(olive::core::internal::Color::from_hsv(h, s, v))); +} + +OakColor *oakcore_color_from_data(const char *data, int format, int nb_channels) +{ + return wrap(new olive::core::internal::Color( + olive::core::internal::Color::from_data(data, to_format(format), + static_cast(nb_channels)))); +} + +float oakcore_color_red(const OakColor *self) +{ + return impl(self)->red(); +} + +float oakcore_color_green(const OakColor *self) +{ + return impl(self)->green(); +} + +float oakcore_color_blue(const OakColor *self) +{ + return impl(self)->blue(); +} + +float oakcore_color_alpha(const OakColor *self) +{ + return impl(self)->alpha(); +} + +void oakcore_color_set_red(OakColor *self, float red) +{ + impl(self)->set_red(red); +} + +void oakcore_color_set_green(OakColor *self, float green) +{ + impl(self)->set_green(green); +} + +void oakcore_color_set_blue(OakColor *self, float blue) +{ + impl(self)->set_blue(blue); +} + +void oakcore_color_set_alpha(OakColor *self, float alpha) +{ + impl(self)->set_alpha(alpha); +} + +void oakcore_color_to_hsv(const OakColor *self, float *hue, float *sat, float *val) +{ + impl(self)->to_hsv(hue, sat, val); +} + +float oakcore_color_hsv_hue(const OakColor *self) +{ + return impl(self)->hsv_hue(); +} + +float oakcore_color_hsv_saturation(const OakColor *self) +{ + return impl(self)->hsv_saturation(); +} + +float oakcore_color_value(const OakColor *self) +{ + return impl(self)->value(); +} + +void oakcore_color_to_hsl(const OakColor *self, float *hue, float *sat, + float *lightness) +{ + impl(self)->to_hsl(hue, sat, lightness); +} + +float oakcore_color_hsl_hue(const OakColor *self) +{ + return impl(self)->hsl_hue(); +} + +float oakcore_color_hsl_saturation(const OakColor *self) +{ + return impl(self)->hsl_saturation(); +} + +float oakcore_color_lightness(const OakColor *self) +{ + return impl(self)->lightness(); +} + +float *oakcore_color_data(OakColor *self) +{ + return impl(self)->data(); +} + +const float *oakcore_color_const_data(const OakColor *self) +{ + return impl(self)->data(); +} + +void oakcore_color_to_data(const OakColor *self, char *out, int format, + int nb_channels) +{ + impl(self)->to_data(out, to_format(format), + static_cast(nb_channels)); +} + +float oakcore_color_get_rough_luminance(const OakColor *self) +{ + return impl(self)->get_rough_luminance(); +} + +void oakcore_color_add_assign(OakColor *self, const OakColor *other) +{ + *impl(self) += *impl(other); +} + +void oakcore_color_sub_assign(OakColor *self, const OakColor *other) +{ + *impl(self) -= *impl(other); +} + +void oakcore_color_add_scalar_assign(OakColor *self, float value) +{ + *impl(self) += value; +} + +void oakcore_color_sub_scalar_assign(OakColor *self, float value) +{ + *impl(self) -= value; +} + +void oakcore_color_mul_scalar_assign(OakColor *self, float value) +{ + *impl(self) *= value; +} + +void oakcore_color_div_scalar_assign(OakColor *self, float value) +{ + *impl(self) /= value; +} + +} // extern "C" diff --git a/core/src/capi/fractionutils.cpp b/core/src/capi/fractionutils.cpp new file mode 100644 index 000000000..2fa54998c --- /dev/null +++ b/core/src/capi/fractionutils.cpp @@ -0,0 +1,53 @@ +/*** + + 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 "oakcore/fractionutils.h" + +#include "util/fractionutils.h" + +namespace +{ + +olive::core::internal::FractionRounding impl(OakFractionRounding rnd) +{ + return static_cast(rnd); +} + +} // namespace + +extern "C" +{ + +void oakcore_fractionutils_reduce_fraction(int64_t *num, int64_t *den, int64_t max) +{ + olive::core::internal::reduce_fraction(*num, *den, max); +} + +int oakcore_fractionutils_compare_fractions(int an, int ad, int bn, int bd) +{ + return olive::core::internal::compare_fractions(an, ad, bn, bd); +} + +int64_t oakcore_fractionutils_rescale_rnd(int64_t a, int64_t b, int64_t c, OakFractionRounding rnd) +{ + return olive::core::internal::rescale_rnd(a, b, c, impl(rnd)); +} + +} // extern "C" diff --git a/core/src/capi/rational.cpp b/core/src/capi/rational.cpp new file mode 100644 index 000000000..92e958ca0 --- /dev/null +++ b/core/src/capi/rational.cpp @@ -0,0 +1,172 @@ +/*** + + 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 "oakcore/rational.h" + +#include + +#include "util/rational.h" + +namespace +{ + +olive::core::internal::Rational *impl(OakRational *h) +{ + return reinterpret_cast(h); +} + +const olive::core::internal::Rational *impl(const OakRational *h) +{ + return reinterpret_cast(h); +} + +OakRational *wrap(olive::core::internal::Rational *r) +{ + return reinterpret_cast(r); +} + +} // namespace + +extern "C" +{ + +OakRational *oakcore_rational_create(int numerator) +{ + return wrap(new olive::core::internal::Rational(numerator)); +} + +OakRational *oakcore_rational_create_nd(int numerator, int denominator) +{ + return wrap(new olive::core::internal::Rational(numerator, denominator)); +} + +OakRational *oakcore_rational_create_nan(void) +{ + return wrap(new olive::core::internal::Rational(olive::core::internal::Rational::na_n)); +} + +OakRational *oakcore_rational_copy(const OakRational *self) +{ + return wrap(new olive::core::internal::Rational(*impl(self))); +} + +void oakcore_rational_free(OakRational *self) +{ + delete impl(self); +} + +int oakcore_rational_numerator(const OakRational *self) +{ + return impl(self)->numerator(); +} + +int oakcore_rational_denominator(const OakRational *self) +{ + return impl(self)->denominator(); +} + +double oakcore_rational_to_double(const OakRational *self) +{ + return impl(self)->to_double(); +} + +int oakcore_rational_to_string(const OakRational *self, char *buf, int buf_size) +{ + const std::string s = impl(self)->to_string(); + if (buf && buf_size > 0) { + snprintf(buf, size_t(buf_size), "%s", s.c_str()); + } + return int(s.size()); +} + +OakRational *oakcore_rational_from_double(double value, int *ok) +{ + bool b = false; + olive::core::internal::Rational *r = + new olive::core::internal::Rational(olive::core::internal::Rational::from_double(value, &b)); + if (ok) { + *ok = b ? 1 : 0; + } + return wrap(r); +} + +OakRational *oakcore_rational_from_string(const char *str, int *ok) +{ + bool b = false; + olive::core::internal::Rational *r = new olive::core::internal::Rational( + olive::core::internal::Rational::from_string(str ? str : "", &b)); + if (ok) { + *ok = b ? 1 : 0; + } + return wrap(r); +} + +int oakcore_rational_is_null(const OakRational *self) +{ + return impl(self)->isNull() ? 1 : 0; +} + +int oakcore_rational_is_nan(const OakRational *self) +{ + return impl(self)->isNaN() ? 1 : 0; +} + +OakRational *oakcore_rational_flipped(const OakRational *self) +{ + return wrap(new olive::core::internal::Rational(impl(self)->flipped())); +} + +void oakcore_rational_flip(OakRational *self) +{ + impl(self)->flip(); +} + +void oakcore_rational_add_assign(OakRational *self, const OakRational *other) +{ + *impl(self) += *impl(other); +} + +void oakcore_rational_sub_assign(OakRational *self, const OakRational *other) +{ + *impl(self) -= *impl(other); +} + +void oakcore_rational_mul_assign(OakRational *self, const OakRational *other) +{ + *impl(self) *= *impl(other); +} + +void oakcore_rational_div_assign(OakRational *self, const OakRational *other) +{ + *impl(self) /= *impl(other); +} + +int oakcore_rational_compare(const OakRational *self, const OakRational *other) +{ + if (*impl(self) < *impl(other)) { + return -1; + } + if (*impl(other) < *impl(self)) { + return 1; + } + return 0; +} + +} // extern "C" diff --git a/core/src/capi/samplebuffer.cpp b/core/src/capi/samplebuffer.cpp new file mode 100644 index 000000000..1dc28a795 --- /dev/null +++ b/core/src/capi/samplebuffer.cpp @@ -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 . + +***/ + +#include "oakcore/samplebuffer.h" + +#include +#include + +#include "render/audioparams.h" +#include "render/samplebuffer.h" +#include "util/rational.h" + +namespace +{ + +olive::core::internal::SampleBuffer *impl(OakSampleBuffer *h) +{ + return reinterpret_cast(h); +} + +const olive::core::internal::SampleBuffer *impl(const OakSampleBuffer *h) +{ + return reinterpret_cast(h); +} + +OakSampleBuffer *wrap(olive::core::internal::SampleBuffer *b) +{ + return reinterpret_cast(b); +} + +const olive::core::internal::AudioParams *apimpl(const OakAudioParams *h) +{ + return reinterpret_cast(h); +} + +OakAudioParams *apwrap(olive::core::internal::AudioParams *p) +{ + return reinterpret_cast(p); +} + +const olive::core::internal::Rational *rimpl(const OakRational *h) +{ + return reinterpret_cast(h); +} + +} // namespace + +extern "C" +{ + +OakSampleBuffer *oakcore_samplebuffer_create(void) +{ + return wrap(new olive::core::internal::SampleBuffer()); +} + +OakSampleBuffer *oakcore_samplebuffer_create_length( + const OakAudioParams *params, const OakRational *length) +{ + return wrap(new olive::core::internal::SampleBuffer(*apimpl(params), + *rimpl(length))); +} + +OakSampleBuffer *oakcore_samplebuffer_create_samples( + const OakAudioParams *params, size_t samples_per_channel) +{ + return wrap(new olive::core::internal::SampleBuffer(*apimpl(params), + samples_per_channel)); +} + +OakSampleBuffer *oakcore_samplebuffer_copy(const OakSampleBuffer *self) +{ + return wrap(new olive::core::internal::SampleBuffer(*impl(self))); +} + +void oakcore_samplebuffer_free(OakSampleBuffer *self) +{ + delete impl(self); +} + +OakSampleBuffer *oakcore_samplebuffer_rip_channel(const OakSampleBuffer *self, + int channel) +{ + return wrap( + new olive::core::internal::SampleBuffer(impl(self)->rip_channel(channel))); +} + +int oakcore_samplebuffer_rip_channel_vector(const OakSampleBuffer *self, + int channel, float *out, + int out_size) +{ + const std::vector v = impl(self)->rip_channel_vector(channel); + if (out && out_size > 0) { + const size_t n = std::min(v.size(), size_t(out_size)); + std::copy(v.begin(), v.begin() + n, out); + } + return int(v.size()); +} + +OakAudioParams *oakcore_samplebuffer_audio_params(const OakSampleBuffer *self) +{ + return apwrap( + new olive::core::internal::AudioParams(impl(self)->audio_params())); +} + +void oakcore_samplebuffer_set_audio_params(OakSampleBuffer *self, + const OakAudioParams *params) +{ + impl(self)->set_audio_params(*apimpl(params)); +} + +size_t oakcore_samplebuffer_sample_count(const OakSampleBuffer *self) +{ + return impl(self)->sample_count(); +} + +void oakcore_samplebuffer_set_sample_count(OakSampleBuffer *self, + size_t sample_count) +{ + impl(self)->set_sample_count(sample_count); +} + +void oakcore_samplebuffer_set_sample_count_length(OakSampleBuffer *self, + const OakRational *length) +{ + impl(self)->set_sample_count(*rimpl(length)); +} + +float *oakcore_samplebuffer_data(OakSampleBuffer *self, int channel) +{ + if (!impl(self)->is_allocated() || channel < 0 || + channel >= impl(self)->channel_count()) { + return nullptr; + } + return impl(self)->data(channel); +} + +void oakcore_samplebuffer_to_raw_ptrs(OakSampleBuffer *self, float **out) +{ + if (!out) { + return; + } + const std::vector ptrs = impl(self)->to_raw_ptrs(); + std::copy(ptrs.begin(), ptrs.end(), out); +} + +int oakcore_samplebuffer_channel_count(const OakSampleBuffer *self) +{ + return impl(self)->channel_count(); +} + +int oakcore_samplebuffer_is_allocated(const OakSampleBuffer *self) +{ + return impl(self)->is_allocated() ? 1 : 0; +} + +void oakcore_samplebuffer_allocate(OakSampleBuffer *self) +{ + impl(self)->allocate(); +} + +void oakcore_samplebuffer_destroy(OakSampleBuffer *self) +{ + impl(self)->destroy(); +} + +void oakcore_samplebuffer_reverse(OakSampleBuffer *self) +{ + impl(self)->reverse(); +} + +void oakcore_samplebuffer_speed(OakSampleBuffer *self, double speed) +{ + impl(self)->speed(speed); +} + +void oakcore_samplebuffer_transform_volume(OakSampleBuffer *self, float f) +{ + impl(self)->transform_volume(f); +} + +void oakcore_samplebuffer_transform_volume_for_channel(OakSampleBuffer *self, + int channel, + float volume) +{ + impl(self)->transform_volume_for_channel(channel, volume); +} + +void oakcore_samplebuffer_transform_volume_to(float f, + const OakSampleBuffer *input, + OakSampleBuffer *output) +{ + olive::core::internal::SampleBuffer::transform_volume(f, impl(input), + impl(output)); +} + +void oakcore_samplebuffer_transform_volume_for_channel_to( + int channel, float volume, const OakSampleBuffer *input, + OakSampleBuffer *output) +{ + olive::core::internal::SampleBuffer::transform_volume_for_channel( + channel, volume, impl(input), impl(output)); +} + +void oakcore_samplebuffer_transform_volume_for_sample(OakSampleBuffer *self, + size_t sample_index, + float volume) +{ + impl(self)->transform_volume_for_sample(sample_index, volume); +} + +void oakcore_samplebuffer_transform_volume_for_sample_on_channel( + OakSampleBuffer *self, size_t sample_index, int channel, float volume) +{ + impl(self)->transform_volume_for_sample_on_channel(sample_index, channel, + volume); +} + +void oakcore_samplebuffer_clamp(OakSampleBuffer *self) +{ + impl(self)->clamp(); +} + +void oakcore_samplebuffer_silence(OakSampleBuffer *self) +{ + impl(self)->silence(); +} + +void oakcore_samplebuffer_silence_range(OakSampleBuffer *self, + size_t start_sample, + size_t end_sample) +{ + impl(self)->silence(start_sample, end_sample); +} + +void oakcore_samplebuffer_silence_bytes(OakSampleBuffer *self, + size_t start_byte, size_t end_byte) +{ + impl(self)->silence_bytes(start_byte, end_byte); +} + +void oakcore_samplebuffer_set(OakSampleBuffer *self, int channel, + const float *data, size_t sample_offset, + size_t sample_length) +{ + impl(self)->set(channel, data, sample_offset, sample_length); +} + +void oakcore_samplebuffer_fast_set(OakSampleBuffer *self, + const OakSampleBuffer *other, int to, + int from) +{ + impl(self)->fast_set(*impl(other), to, from); +} + +} // extern "C" diff --git a/core/src/capi/stringutils.cpp b/core/src/capi/stringutils.cpp new file mode 100644 index 000000000..00db947e0 --- /dev/null +++ b/core/src/capi/stringutils.cpp @@ -0,0 +1,123 @@ +/*** + + 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 "oakcore/stringutils.h" + +#include +#include +#include + +#include +#include +#include + +#include "util/stringutils.h" + +namespace +{ + +char **copy_string_vector(const std::vector &v, int *count) +{ + char **arr = static_cast(malloc(sizeof(char *) * v.size())); + if (!arr) { + *count = 0; + return nullptr; + } + for (size_t i = 0; i < v.size(); i++) { + arr[i] = static_cast(malloc(v[i].size() + 1)); + if (!arr[i]) { + for (size_t j = 0; j < i; j++) { + free(arr[j]); + } + free(arr); + *count = 0; + return nullptr; + } + memcpy(arr[i], v[i].c_str(), v[i].size() + 1); + } + *count = int(v.size()); + return arr; +} + +} // namespace + +extern "C" +{ + +char **oakcore_stringutils_split(const char *s, char separator, int *count) +{ + const std::vector v = + olive::core::internal::StringUtils::split(s ? s : "", separator); + return copy_string_vector(v, count); +} + +char **oakcore_stringutils_split_regex(const char *s, const char *pattern, + int *count) +{ + const std::vector v = + olive::core::internal::StringUtils::split_regex( + s ? s : "", std::regex(pattern ? pattern : "")); + return copy_string_vector(v, count); +} + +void oakcore_stringutils_free_string_array(char **arr, int count) +{ + if (!arr) { + return; + } + for (int i = 0; i < count; i++) { + free(arr[i]); + } + free(arr); +} + +int oakcore_stringutils_to_int(const char *s, int base, int *ok) +{ + bool b = false; + const int x = olive::core::internal::StringUtils::to_int(s ? s : "", base, &b); + if (ok) { + *ok = b ? 1 : 0; + } + return x; +} + +int oakcore_stringutils_format(char *buf, int buf_size, const char *fmt, ...) +{ + va_list args; + va_start(args, fmt); + const int r = oakcore_stringutils_format_v(buf, buf_size, fmt, args); + va_end(args); + return r; +} + +int oakcore_stringutils_format_v(char *buf, int buf_size, const char *fmt, + va_list args) +{ + // The implementation class only exposes a variadic format(), so the + // va_list form applies the same vsnprintf semantics directly here. + va_list copy; + va_copy(copy, args); + const int needed = + vsnprintf(buf, buf_size > 0 ? size_t(buf_size) : 0, fmt, copy); + va_end(copy); + return needed; +} + +} // extern "C" diff --git a/core/src/capi/timecodefunctions.cpp b/core/src/capi/timecodefunctions.cpp new file mode 100644 index 000000000..05057ff25 --- /dev/null +++ b/core/src/capi/timecodefunctions.cpp @@ -0,0 +1,157 @@ +/*** + + 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 "oakcore/timecodefunctions.h" + +#include + +#include + +#include "util/rational.h" +#include "util/timecodefunctions.h" + +namespace +{ + +const olive::core::internal::Rational *impl(const OakRational *h) +{ + return reinterpret_cast(h); +} + +OakRational *wrap(olive::core::internal::Rational *r) +{ + return reinterpret_cast(r); +} + +olive::core::internal::Timecode::Display to_display(OakTimecodeDisplay d) +{ + return static_cast(d); +} + +olive::core::internal::Timecode::Rounding to_rounding(OakTimecodeRounding r) +{ + return static_cast(r); +} + +int write_string(const std::string &s, char *buf, int buf_size) +{ + if (buf && buf_size > 0) { + snprintf(buf, size_t(buf_size), "%s", s.c_str()); + } + return int(s.size()); +} + +} // namespace + +extern "C" +{ + +int oakcore_timecode_time_to_timecode(const OakRational *time, + const OakRational *timebase, + OakTimecodeDisplay display, + int show_plus_if_positive, char *buf, + int buf_size) +{ + const std::string s = olive::core::internal::Timecode::time_to_timecode( + *impl(time), *impl(timebase), to_display(display), + show_plus_if_positive != 0); + return write_string(s, buf, buf_size); +} + +OakRational *oakcore_timecode_timecode_to_time(const char *timecode, + const OakRational *timebase, + OakTimecodeDisplay display, + int *ok) +{ + bool b = false; + olive::core::internal::Rational *r = new olive::core::internal::Rational( + olive::core::internal::Timecode::timecode_to_time( + timecode ? timecode : "", *impl(timebase), to_display(display), + &b)); + if (ok) { + *ok = b ? 1 : 0; + } + return wrap(r); +} + +int oakcore_timecode_time_to_string(int64_t ms, char *buf, int buf_size) +{ + return write_string(olive::core::internal::Timecode::time_to_string(ms), + buf, buf_size); +} + +OakRational *oakcore_timecode_snap_time_to_timebase(const OakRational *time, + const OakRational *timebase, + OakTimecodeRounding rounding) +{ + return wrap(new olive::core::internal::Rational( + olive::core::internal::Timecode::snap_time_to_timebase( + *impl(time), *impl(timebase), to_rounding(rounding)))); +} + +int64_t oakcore_timecode_time_to_timestamp(const OakRational *time, + const OakRational *timebase, + OakTimecodeRounding rounding) +{ + return olive::core::internal::Timecode::time_to_timestamp( + *impl(time), *impl(timebase), to_rounding(rounding)); +} + +int64_t oakcore_timecode_time_to_timestamp_d(double time, + const OakRational *timebase, + OakTimecodeRounding rounding) +{ + return olive::core::internal::Timecode::time_to_timestamp( + time, *impl(timebase), to_rounding(rounding)); +} + +int64_t oakcore_timecode_rescale_timestamp(int64_t ts, + const OakRational *source, + const OakRational *dest) +{ + return olive::core::internal::Timecode::rescale_timestamp( + ts, *impl(source), *impl(dest)); +} + +int64_t oakcore_timecode_rescale_timestamp_ceil(int64_t ts, + const OakRational *source, + const OakRational *dest) +{ + return olive::core::internal::Timecode::rescale_timestamp_ceil( + ts, *impl(source), *impl(dest)); +} + +OakRational *oakcore_timecode_timestamp_to_time(int64_t timestamp, + const OakRational *timebase) +{ + return wrap(new olive::core::internal::Rational( + olive::core::internal::Timecode::timestamp_to_time(timestamp, + *impl(timebase)))); +} + +int oakcore_timecode_timebase_is_drop_frame(const OakRational *timebase) +{ + return olive::core::internal::Timecode::timebase_is_drop_frame( + *impl(timebase)) + ? 1 + : 0; +} + +} // extern "C" diff --git a/core/src/capi/timerange.cpp b/core/src/capi/timerange.cpp new file mode 100644 index 000000000..916595a0f --- /dev/null +++ b/core/src/capi/timerange.cpp @@ -0,0 +1,221 @@ +/*** + + 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 "oakcore/timerange.h" + +#include + +#include "util/rational.h" +#include "util/timerange.h" + +namespace +{ + +olive::core::internal::TimeRange *impl(OakTimeRange *h) +{ + return reinterpret_cast(h); +} + +const olive::core::internal::TimeRange *impl(const OakTimeRange *h) +{ + return reinterpret_cast(h); +} + +OakTimeRange *wrap(olive::core::internal::TimeRange *r) +{ + return reinterpret_cast(r); +} + +const olive::core::internal::Rational *rimpl(const OakRational *h) +{ + return reinterpret_cast(h); +} + +OakRational *rwrap(olive::core::internal::Rational *r) +{ + return reinterpret_cast(r); +} + +} // namespace + +extern "C" +{ + +OakTimeRange *oakcore_timerange_create(void) +{ + return wrap(new olive::core::internal::TimeRange()); +} + +OakTimeRange *oakcore_timerange_create_io(const OakRational *in, + const OakRational *out) +{ + return wrap( + new olive::core::internal::TimeRange(*rimpl(in), *rimpl(out))); +} + +OakTimeRange *oakcore_timerange_copy(const OakTimeRange *self) +{ + return wrap(new olive::core::internal::TimeRange(*impl(self))); +} + +void oakcore_timerange_free(OakTimeRange *self) +{ + delete impl(self); +} + +OakRational *oakcore_timerange_in(const OakTimeRange *self) +{ + return rwrap(new olive::core::internal::Rational(impl(self)->in())); +} + +OakRational *oakcore_timerange_out(const OakTimeRange *self) +{ + return rwrap(new olive::core::internal::Rational(impl(self)->out())); +} + +OakRational *oakcore_timerange_length(const OakTimeRange *self) +{ + return rwrap(new olive::core::internal::Rational(impl(self)->length())); +} + +void oakcore_timerange_set_in(OakTimeRange *self, const OakRational *in) +{ + impl(self)->set_in(*rimpl(in)); +} + +void oakcore_timerange_set_out(OakTimeRange *self, const OakRational *out) +{ + impl(self)->set_out(*rimpl(out)); +} + +void oakcore_timerange_set_range(OakTimeRange *self, const OakRational *in, + const OakRational *out) +{ + impl(self)->set_range(*rimpl(in), *rimpl(out)); +} + +int oakcore_timerange_equal(const OakTimeRange *self, + const OakTimeRange *other) +{ + return *impl(self) == *impl(other) ? 1 : 0; +} + +int oakcore_timerange_overlaps_with(const OakTimeRange *self, + const OakTimeRange *other, + int in_inclusive, int out_inclusive) +{ + return impl(self) + ->overlaps_with(*impl(other), in_inclusive != 0, + out_inclusive != 0) ? + 1 : + 0; +} + +int oakcore_timerange_contains_range(const OakTimeRange *self, + const OakTimeRange *other, + int in_inclusive, int out_inclusive) +{ + return impl(self) + ->contains(*impl(other), in_inclusive != 0, + out_inclusive != 0) ? + 1 : + 0; +} + +int oakcore_timerange_contains_time(const OakTimeRange *self, + const OakRational *time) +{ + return impl(self)->contains(*rimpl(time)) ? 1 : 0; +} + +OakTimeRange *oakcore_timerange_combined(const OakTimeRange *self, + const OakTimeRange *other) +{ + return wrap( + new olive::core::internal::TimeRange(impl(self)->combined(*impl(other)))); +} + +OakTimeRange *oakcore_timerange_combine(const OakTimeRange *a, + const OakTimeRange *b) +{ + return wrap(new olive::core::internal::TimeRange( + olive::core::internal::TimeRange::combine(*impl(a), *impl(b)))); +} + +OakTimeRange *oakcore_timerange_intersected(const OakTimeRange *self, + const OakTimeRange *other) +{ + return wrap(new olive::core::internal::TimeRange( + impl(self)->intersected(*impl(other)))); +} + +OakTimeRange *oakcore_timerange_intersect(const OakTimeRange *a, + const OakTimeRange *b) +{ + return wrap(new olive::core::internal::TimeRange( + olive::core::internal::TimeRange::intersect(*impl(a), *impl(b)))); +} + +OakTimeRange *oakcore_timerange_add(const OakTimeRange *self, + const OakRational *rhs) +{ + return wrap( + new olive::core::internal::TimeRange(*impl(self) + *rimpl(rhs))); +} + +OakTimeRange *oakcore_timerange_sub(const OakTimeRange *self, + const OakRational *rhs) +{ + return wrap( + new olive::core::internal::TimeRange(*impl(self) - *rimpl(rhs))); +} + +void oakcore_timerange_add_assign(OakTimeRange *self, const OakRational *rhs) +{ + *impl(self) += *rimpl(rhs); +} + +void oakcore_timerange_sub_assign(OakTimeRange *self, const OakRational *rhs) +{ + *impl(self) -= *rimpl(rhs); +} + +int oakcore_timerange_split_count(const OakTimeRange *self, int chunk_size) +{ + return int(impl(self)->split(chunk_size).size()); +} + +int oakcore_timerange_split(const OakTimeRange *self, int chunk_size, + OakTimeRange **out_ranges, int out_size) +{ + const std::list ranges = + impl(self)->split(chunk_size); + int n = 0; + for (const olive::core::internal::TimeRange &r : ranges) { + if (out_ranges && n < out_size) { + out_ranges[n] = + wrap(new olive::core::internal::TimeRange(r)); + } + n++; + } + return n; +} + +} // extern "C" diff --git a/core/src/oliveimpl/render/audioparams.h b/core/src/oliveimpl/render/audioparams.h new file mode 100644 index 000000000..8b73d2304 --- /dev/null +++ b/core/src/oliveimpl/render/audioparams.h @@ -0,0 +1,217 @@ +/*** + + Olive - Non-Linear Video Editor + Copyright (C) 2023 Olive Studios LLC + Modifications Copyright (C) 2025 mikesolar + + This program is free software: you can redistribute it and/or modify + it under the terms of the GNU General Public License as published by + the Free Software Foundation, either version 3 of the License, or + (at your option) any later version. + + This program is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU General Public License for more details. + + You should have received a copy of the GNU General Public License + along with this program. If not, see . + +***/ + +#ifndef OAK_LIBOLIVECORE_AUDIOPARAMS_H +#define OAK_LIBOLIVECORE_AUDIOPARAMS_H +#include + +#include +#include + +#include "render/channellayout.h" +#include "render/sampleformat.h" +#include "../util/rational.h" + +namespace olive::core::internal +{ + +/** + * @brief Audio parameters class managing audio stream configuration + * + * Channel layouts are stored as plain 64-bit masks (see channellayout.h). + * Because the mask is a simple value type, AudioParams has value semantics + * and can be copied freely. + */ +class AudioParams { +public: + /** + * @brief Default constructor creates invalid AudioParams + * sample_rate=0, channel_layout empty, format=INVALID + */ + AudioParams() + : sample_rate_(0) + , channel_layout_mask_(0) + , channel_count_(0) + , format_(SampleFormat::invalid) + { + set_default_footage_parameters(); + } + + /** + * @brief Constructor from channel layout mask + * @param sample_rate Audio sample rate + * @param channel_layout Channel layout mask (e.g., kChannelLayoutStereo) + * @param format Sample format + */ + AudioParams(const int &sample_rate, uint64_t channel_layout, + const SampleFormat &format) + : sample_rate_(sample_rate) + , channel_layout_mask_(channel_layout) + , channel_count_(0) + , format_(format) + { + set_default_footage_parameters(); + timebase_ = sample_rate_as_time_base(); + calculate_channel_count(); + } + int sample_rate() const + { + return sample_rate_; + } + + void set_sample_rate(int sample_rate) + { + sample_rate_ = sample_rate; + } + + /** + * @brief Channel layout as a 64-bit mask (0 if unspecified) + */ + const uint64_t &channel_layout() const + { + return channel_layout_mask_; + } + + /** + * @brief Set channel layout from mask + * @param mask Channel layout mask (e.g., kChannelLayoutStereo) + */ + void set_channel_layout(uint64_t mask) + { + channel_layout_mask_ = mask; + calculate_channel_count(); + } + Rational time_base() const + { + return timebase_; + } + + void set_time_base(const Rational &timebase) + { + timebase_ = timebase; + } + + Rational sample_rate_as_time_base() const + { + return Rational(1, sample_rate()); + } + + SampleFormat format() const + { + return format_; + } + + void set_format(SampleFormat format) + { + format_ = format; + } + + bool enabled() const + { + return enabled_; + } + + void set_enabled(bool e) + { + enabled_ = e; + } + + int stream_index() const + { + return stream_index_; + } + + void set_stream_index(int s) + { + stream_index_ = s; + } + + int64_t duration() const + { + return duration_; + } + + void set_duration(int64_t duration) + { + duration_ = duration; + } + + int64_t time_to_bytes(const double &time) const; + int64_t time_to_bytes(const Rational &time) const; + int64_t time_to_bytes_per_channel(const double &time) const; + int64_t time_to_bytes_per_channel(const Rational &time) const; + int64_t time_to_samples(const double &time) const; + int64_t time_to_samples(const Rational &time) const; + int64_t samples_to_bytes(const int64_t &samples) const; + int64_t samples_to_bytes_per_channel(const int64_t &samples) const; + Rational samples_to_time(const int64_t &samples) const; + int64_t bytes_to_samples(const int64_t &bytes) const; + Rational bytes_to_time(const int64_t &bytes) const; + Rational bytes_per_channel_to_time(const int64_t &bytes) const; + int channel_count() const; + int bytes_per_sample_per_channel() const; + int bits_per_sample() const; + bool is_valid() const; + + bool operator==(const AudioParams &other) const; + bool operator!=(const AudioParams &other) const; + + static const std::vector k_supported_channel_layouts; + static const std::vector k_supported_sample_rates; + +private: + void set_default_footage_parameters() + { + enabled_ = true; + stream_index_ = 0; + duration_ = 0; + } + + /** + * @brief Updates channel_count_ from the current channel_layout_mask_ + * Called after any channel layout modification. + */ + void calculate_channel_count(); + + int sample_rate_; ///< Audio sample rate in Hz (e.g., 48000) + + /** + * @brief Channel layout mask (0 if unspecified) + * + * Plain value type mirroring FFmpeg's AV_CH_LAYOUT_* masks; no dynamic + * memory is involved, so copies are trivially safe. + */ + uint64_t channel_layout_mask_; + + int channel_count_; ///< Cached channel count from layout + + SampleFormat format_; ///< Audio sample format + + // Footage-specific parameters (serialized with footage metadata) + int enabled_; // Using int instead of bool fixes GCC 11 stringop-overflow issue (byte alignment) + int stream_index_; ///< Index in the source file's stream list + int64_t duration_; ///< Stream duration in timebase units + Rational timebase_; ///< Timebase for this audio stream +}; + +} + +#endif // OAK_LIBOLIVECORE_AUDIOPARAMS_H diff --git a/core/src/oliveimpl/render/samplebuffer.h b/core/src/oliveimpl/render/samplebuffer.h new file mode 100644 index 000000000..a2448271d --- /dev/null +++ b/core/src/oliveimpl/render/samplebuffer.h @@ -0,0 +1,137 @@ +/*** + + Olive - Non-Linear Video Editor + Copyright (C) 2023 Olive Studios LLC + Modifications Copyright (C) 2025 mikesolar + + This program is free software: you can redistribute it and/or modify + it under the terms of the GNU General Public License as published by + the Free Software Foundation, either version 3 of the License, or + (at your option) any later version. + + This program is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU General Public License for more details. + + You should have received a copy of the GNU General Public License + along with this program. If not, see . + +***/ + +#ifndef OAK_LIBOLIVECORE_SAMPLEBUFFER_H +#define OAK_LIBOLIVECORE_SAMPLEBUFFER_H + +#include +#include + +#include "audioparams.h" +#include "../util/rational.h" + +namespace olive::core::internal +{ + +/** + * @brief A buffer of audio samples + * + * Audio samples in this structure are always stored in PLANAR (separated by channel). This is done to simplify audio + * rendering code. This replaces the old system of using QByteArrays (containing packed audio) and while SampleBuffer + * replaces many of those in the rendering/processing side of things, QByteArrays are currently still in use for + * playback, including reading to and from the cache. + */ +class SampleBuffer { +public: + SampleBuffer(); + SampleBuffer(const AudioParams &audio_params, const Rational &length); + SampleBuffer(const AudioParams &audio_params, size_t samples_per_channel); + + SampleBuffer rip_channel(int channel) const; + std::vector rip_channel_vector(int channel) const; + + const AudioParams &audio_params() const; + void set_audio_params(const AudioParams ¶ms); + + const size_t &sample_count() const + { + return sample_count_per_channel_; + } + void set_sample_count(const size_t &sample_count); + void set_sample_count(const Rational &length) + { + set_sample_count(audio_params_.time_to_samples(length)); + } + + float *data(int channel) + { + return data_[channel].data(); + } + + const float *data(int channel) const + { + return data_.at(channel).data(); + } + + std::vector to_raw_ptrs() + { + std::vector r(data_.size()); + for (size_t i = 0; i < r.size(); i++) { + r[i] = data_[i].data(); + } + return r; + } + + int channel_count() const + { + return data_.size(); + } + + bool is_allocated() const + { + return !data_.empty(); + } + void allocate(); + void destroy(); + + void reverse(); + void speed(double speed); + + void transform_volume(float f); + void transform_volume_for_channel(int channel, float volume); + static void transform_volume(float f, const SampleBuffer *input, + SampleBuffer *output); + static void transform_volume_for_channel(int channel, float volume, + const SampleBuffer *input, + SampleBuffer *output); + + void transform_volume_for_sample(size_t sample_index, float volume); + void transform_volume_for_sample_on_channel(size_t sample_index, + int channel, float volume); + + void clamp(); + + void silence(); + void silence(size_t start_sample, size_t end_sample); + void silence_bytes(size_t start_byte, size_t end_byte); + + void set(int channel, const float *data, size_t sample_offset, + size_t sample_length); + void set(int channel, const float *data, size_t sample_length) + { + set(channel, data, 0, sample_length); + } + + void fast_set(const SampleBuffer &other, int to, int from = -1); + +private: + void clamp_channel(int channel); + + AudioParams audio_params_; + + size_t sample_count_per_channel_; + + std::vector> data_; +}; + +} + +#endif // OAK_LIBOLIVECORE_SAMPLEBUFFER_H diff --git a/core/src/oliveimpl/util/bezier.h b/core/src/oliveimpl/util/bezier.h new file mode 100644 index 000000000..dcd2e1569 --- /dev/null +++ b/core/src/oliveimpl/util/bezier.h @@ -0,0 +1,138 @@ +/*** + + Olive - Non-Linear Video Editor + Copyright (C) 2023 Olive Studios LLC + Modifications Copyright (C) 2025 mikesolar + + This program is free software: you can redistribute it and/or modify + it under the terms of the GNU General Public License as published by + the Free Software Foundation, either version 3 of the License, or + (at your option) any later version. + + This program is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU General Public License for more details. + + You should have received a copy of the GNU General Public License + along with this program. If not, see . + +***/ + +#ifndef OAK_LIBOLIVECORE_BEZIER_H +#define OAK_LIBOLIVECORE_BEZIER_H + +#include + +namespace olive::core::internal +{ + +class Bezier { +public: + Bezier(); + Bezier(double x, double y); + Bezier(double x, double y, double cp1_x, double cp1_y, double cp2_x, + double cp2_y); + + const double &x() const + { + return x_; + } + const double &y() const + { + return y_; + } + const double &cp1_x() const + { + return cp1_x_; + } + const double &cp1_y() const + { + return cp1_y_; + } + const double &cp2_x() const + { + return cp2_x_; + } + const double &cp2_y() const + { + return cp2_y_; + } + + Imath::V2d to_vec() const + { + return Imath::V2d(x_, y_); + } + + Imath::V2d control_point_1_to_vec() const + { + return Imath::V2d(cp1_x_, cp1_y_); + } + + Imath::V2d control_point_2_to_vec() const + { + return Imath::V2d(cp2_x_, cp2_y_); + } + + void set_x(const double &x) + { + x_ = x; + } + void set_y(const double &y) + { + y_ = y; + } + void set_cp1_x(const double &cp1_x) + { + cp1_x_ = cp1_x; + } + void set_cp1_y(const double &cp1_y) + { + cp1_y_ = cp1_y; + } + void set_cp2_x(const double &cp2_x) + { + cp2_x_ = cp2_x; + } + void set_cp2_y(const double &cp2_y) + { + cp2_y_ = cp2_y; + } + + static double quadratic_xto_t(double x, double a, double b, double c); + + static double quadratic_tto_y(double a, double b, double c, double t); + + static double quadratic_xto_y(double x, const Imath::V2d &a, + const Imath::V2d &b, const Imath::V2d &c) + { + return quadratic_tto_y(a.y, b.y, c.y, quadratic_xto_t(x, a.x, b.x, c.x)); + } + + static double cubic_xto_t(double x, double a, double b, double c, double d); + + static double cubic_tto_y(double a, double b, double c, double d, double t); + + static double cubic_xto_y(double x, const Imath::V2d &a, const Imath::V2d &b, + const Imath::V2d &c, const Imath::V2d &d) + { + return cubic_tto_y(a.y, b.y, c.y, d.y, cubic_xto_t(x, a.x, b.x, c.x, d.x)); + } + +private: + static double calculate_t_from_x(bool cubic, double x, double a, double b, + double c, double d); + + double x_; + double y_; + + double cp1_x_; + double cp1_y_; + + double cp2_x_; + double cp2_y_; +}; + +} + +#endif // OAK_LIBOLIVECORE_BEZIER_H diff --git a/core/src/oliveimpl/util/color.h b/core/src/oliveimpl/util/color.h new file mode 100644 index 000000000..3701879d0 --- /dev/null +++ b/core/src/oliveimpl/util/color.h @@ -0,0 +1,184 @@ +/*** + + Olive - Non-Linear Video Editor + Copyright (C) 2023 Olive Studios LLC + Modifications Copyright (C) 2025 mikesolar + + This program is free software: you can redistribute it and/or modify + it under the terms of the GNU General Public License as published by + the Free Software Foundation, either version 3 of the License, or + (at your option) any later version. + + This program is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU General Public License for more details. + + You should have received a copy of the GNU General Public License + along with this program. If not, see . + +***/ + +#ifndef OAK_LIBOLIVECORE_COLOR_H +#define OAK_LIBOLIVECORE_COLOR_H + +#include "render/pixelformat.h" + +namespace olive::core::internal +{ + +/** + * @brief High precision 32-bit DataType based RGBA color value + */ +class Color { +public: + using DataType = float; + static constexpr unsigned int rgba = 4; + + Color() + { + for (unsigned int i = 0; i < rgba; i++) { + data_[i] = 0.0; + } + } + + Color(const DataType &r, const DataType &g, const DataType &b, + const DataType &a = 1.0f) + { + data_[0] = r; + data_[1] = g; + data_[2] = b; + data_[3] = a; + } + + Color(const char *data, const PixelFormat &format, int ch_layout); + + /** + * @brief Creates a Color struct from hue/saturation/value + * + * Hue expects a value between 0.0 and 360.0. Saturation and Value expect a value between 0.0 and 1.0. + */ + static Color from_hsv(const DataType &h, const DataType &s, + const DataType &v); + + const DataType &red() const + { + return data_[0]; + } + const DataType &green() const + { + return data_[1]; + } + const DataType &blue() const + { + return data_[2]; + } + const DataType &alpha() const + { + return data_[3]; + } + + void to_hsv(DataType *hue, DataType *sat, DataType *val) const; + DataType hsv_hue() const; + DataType hsv_saturation() const; + DataType value() const; + + void to_hsl(DataType *hue, DataType *sat, DataType *lightness) const; + DataType hsl_hue() const; + DataType hsl_saturation() const; + DataType lightness() const; + + void set_red(const DataType &red) + { + data_[0] = red; + } + void set_green(const DataType &green) + { + data_[1] = green; + } + void set_blue(const DataType &blue) + { + data_[2] = blue; + } + void set_alpha(const DataType &alpha) + { + data_[3] = alpha; + } + + DataType *data() + { + return data_; + } + const DataType *data() const + { + return data_; + } + + void to_data(char *out, const PixelFormat &format, + unsigned int nb_channels) const; + + static Color from_data(const char *in, const PixelFormat &format, + unsigned int nb_channels); + + // Suuuuper rough luminance value mostly used for UI (determining whether to overlay with black + // or white text) + DataType get_rough_luminance() const; + + // Assignment math operators + Color &operator+=(const Color &rhs); + Color &operator-=(const Color &rhs); + Color &operator+=(const DataType &rhs); + Color &operator-=(const DataType &rhs); + Color &operator*=(const DataType &rhs); + Color &operator/=(const DataType &rhs); + + // Binary math operators + Color operator+(const Color &rhs) const + { + Color c(*this); + c += rhs; + return c; + } + + Color operator-(const Color &rhs) const + { + Color c(*this); + c -= rhs; + return c; + } + + Color operator+(const DataType &rhs) const + { + Color c(*this); + c += rhs; + return c; + } + + Color operator-(const DataType &rhs) const + { + Color c(*this); + c -= rhs; + return c; + } + + Color operator*(const DataType &rhs) const + { + Color c(*this); + c *= rhs; + return c; + } + + Color operator/(const DataType &rhs) const + { + Color c(*this); + c /= rhs; + return c; + } + +private: + DataType data_[rgba]; +}; + +} + +#endif // OAK_LIBOLIVECORE_COLOR_H diff --git a/core/src/oliveimpl/util/fractionutils.h b/core/src/oliveimpl/util/fractionutils.h new file mode 100644 index 000000000..3ee19f9f4 --- /dev/null +++ b/core/src/oliveimpl/util/fractionutils.h @@ -0,0 +1,80 @@ +/*** + + Olive - Non-Linear Video Editor + Copyright (C) 2023 Olive Studios LLC + Modifications Copyright (C) 2025 mikesolar + + This program is free software: you can redistribute it and/or modify + it under the terms of the GNU General Public License as published by + the Free Software Foundation, either version 3 of the License, or + (at your option) any later version. + + This program is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU General Public License for more details. + + You should have received a copy of the GNU General Public License + along with this program. If not, see . + +***/ + +#ifndef OAK_LIBOLIVECORE_FRACTIONUTILS_H +#define OAK_LIBOLIVECORE_FRACTIONUTILS_H + +#include + +namespace olive::core::internal +{ + +/** + * @brief Rounding modes for RescaleRnd() + * + * Mirrors the FFmpeg AVRounding modes that this codebase used before the + * FFmpeg dependency was removed from core. + */ +enum class FractionRounding { + /** + * Round to the nearest value; halfway cases are rounded away from zero. + * Equivalent to FFmpeg's AV_ROUND_NEAR_INF. + */ + k_near_inf, + + /** + * Round toward positive infinity. Equivalent to FFmpeg's AV_ROUND_UP. + */ + k_up +}; + +/** + * @brief Reduce a fraction so that numerator and denominator fit within `max` + * + * Native re-implementation of FFmpeg's av_reduce(): divides out the greatest + * common divisor and, if the values still do not fit within `max`, finds the + * closest approximation using continued fractions. + * + * A zero denominator is preserved (with the numerator set to zero). + */ +void reduce_fraction(int64_t &num, int64_t &den, int64_t max); + +/** + * @brief Compare two fractions + * + * Native re-implementation of FFmpeg's av_cmp_q(): returns -1 if a < b, + * 0 if a == b, 1 if a > b, and INT_MIN when the comparison is meaningless + * (degenerate zero-denominator fractions). + */ +int compare_fractions(int an, int ad, int bn, int bd); + +/** + * @brief Rescale `a` by the fraction b/c: returns a * b / c + * + * Native re-implementation of FFmpeg's av_rescale_rnd(). The intermediate + * product is computed with 128-bit arithmetic where available so that no + * precision is lost for large timestamps. + */ +int64_t rescale_rnd(int64_t a, int64_t b, int64_t c, FractionRounding rnd); + +} + +#endif // OAK_LIBOLIVECORE_FRACTIONUTILS_H diff --git a/core/src/oliveimpl/util/rational.h b/core/src/oliveimpl/util/rational.h new file mode 100644 index 000000000..5809183ce --- /dev/null +++ b/core/src/oliveimpl/util/rational.h @@ -0,0 +1,157 @@ +/*** + + Olive - Non-Linear Video Editor + Copyright (C) 2023 Olive Studios LLC + Modifications Copyright (C) 2025 mikesolar + + This program is free software: you can redistribute it and/or modify + it under the terms of the GNU General Public License as published by + the Free Software Foundation, either version 3 of the License, or + (at your option) any later version. + + This program is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU General Public License for more details. + + You should have received a copy of the GNU General Public License + along with this program. If not, see . + +***/ + +#ifndef OAK_LIBOLIVECORE_RATIONAL_H +#define OAK_LIBOLIVECORE_RATIONAL_H + +#include +#include + +#ifdef USE_OTIO +#include +#endif + +namespace olive::core::internal +{ + +class Rational { +public: + Rational(const int &numerator = 0) + { + num_ = numerator; + den_ = 1; + } + + Rational(const int &numerator, const int &denominator) + { + num_ = numerator; + den_ = denominator; + + fix_signs(); + reduce(); + } + + Rational(const Rational &rhs) = default; + + static Rational from_double(const double &flt, bool *ok = nullptr); + static Rational from_string(const std::string &str, bool *ok = nullptr); + + static const Rational na_n; + + //Assignment Operators + const Rational &operator=(const Rational &rhs); + const Rational &operator+=(const Rational &rhs); + const Rational &operator-=(const Rational &rhs); + const Rational &operator/=(const Rational &rhs); + const Rational &operator*=(const Rational &rhs); + + //Binary math operators + Rational operator+(const Rational &rhs) const; + Rational operator-(const Rational &rhs) const; + Rational operator/(const Rational &rhs) const; + Rational operator*(const Rational &rhs) const; + + //Relational and equality operators + bool operator<(const Rational &rhs) const; + bool operator<=(const Rational &rhs) const; + bool operator>(const Rational &rhs) const; + bool operator>=(const Rational &rhs) const; + bool operator==(const Rational &rhs) const; + bool operator!=(const Rational &rhs) const; + + //Unary operators + const Rational &operator+() const + { + return *this; + } + Rational operator-() const + { + return Rational(num_, -den_); + } + bool operator!() const + { + return !num_; + } + + //Function: convert to double + double to_double() const; + +#ifdef USE_OTIO + static Rational fromRationalTime(const opentime::RationalTime &t) + { + // Is this the best way to do this? + return fromDouble(t.to_seconds()); + } + + // Convert Olive rationals to opentime rationals with the given framerate (defaults to 24) + opentime::RationalTime toRationalTime(double framerate = 24) const; +#endif + + // Produce "flipped" version + Rational flipped() const; + void flip(); + + // Returns whether the Rational is valid but equal to zero or not + // + // A NaN is always a null, but a null is not always a NaN + bool isNull() const + { + return num_ == 0; + } + + // Returns whether this Rational is not a valid number (denominator == 0) + bool isNaN() const + { + return den_ == 0; + } + + const int &numerator() const + { + return num_; + } + const int &denominator() const + { + return den_; + } + + std::string to_string() const; + + friend std::ostream &operator<<(std::ostream &out, const Rational &value) + { + out << value.num_ << '/' << value.den_; + + return out; + } + +private: + void fix_signs(); + void reduce(); + + int num_; + int den_; +}; + +#define RATIONAL_MIN Rational(INT_MIN) +#define RATIONAL_MAX Rational(INT_MAX) + +} + +#endif // OAK_LIBOLIVECORE_RATIONAL_H diff --git a/core/src/oliveimpl/util/stringutils.h b/core/src/oliveimpl/util/stringutils.h new file mode 100644 index 000000000..f76acd966 --- /dev/null +++ b/core/src/oliveimpl/util/stringutils.h @@ -0,0 +1,211 @@ +/*** + + Olive - Non-Linear Video Editor + Copyright (C) 2023 Olive Studios LLC + Modifications Copyright (C) 2025 mikesolar + + This program is free software: you can redistribute it and/or modify + it under the terms of the GNU General Public License as published by + the Free Software Foundation, either version 3 of the License, or + (at your option) any later version. + + This program is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU General Public License for more details. + + You should have received a copy of the GNU General Public License + along with this program. If not, see . + +***/ + +#ifndef OAK_LIBOLIVECORE_STRINGUTILS_H +#define OAK_LIBOLIVECORE_STRINGUTILS_H + +#include +#include +#include +#include + +namespace olive::core::internal +{ + +class StringUtils { +public: + /** + * @brief Split a string into a list of strings using a specific delimiter + * + * @param s + * + * The string to split. + * + * @param separator + * + * The character to split the string on. + * + * @return + * + * A vector of strings split by the specified delimiter. + */ + static std::vector split(const std::string &s, char separator); + + /** + * @brief Splits a string into a list of strings using regular expressions. + * + * @param s + * + * The string to split. + * + * @param regex + * + * The regular expression to split the string on. + * + * @return + * + * A vector of strings split wherever the regular expression matched. + */ + static std::vector split_regex(const std::string &s, + const std::regex ®ex); + + /** + * @brief Convert a string to int using a bool pointer to determine success rather than an exception + * + * @param s + * + * The string to parse an int from. + * + * @param base + * + * The base of the number in the string (usually 10, or 16 for hex). + * + * @param ok + * + * (Optional) a boolean output parameter specifying whether the conversion was successful or not. + * + * @return + * + * Either the int parsed from the string, or 0 (with *ok set to false) on parser error. + */ + static int to_int(const std::string &s, int base, bool *ok = nullptr); + + /** + * @brief Overloaded function + * + * @param s + * + * The string to parse an int from. + * + * @param ok + * + * (Optional) a boolean output parameter specifying whether the conversion was successful or not. + * + * @return + * + * Either the int parsed from the string, or 0 (with *ok set to false) on parser error. + */ + static int to_int(const std::string &s, bool *ok = nullptr) + { + return to_int(s, 10, ok); + } + + /** + * @brief Convert a number to a string with left padding + * + * Usually used for converting a number to a string with leading zeroes. + * + * @param val + * + * The number to convert. This is a templated function and will accept any type, e.g. + * int/long/float/double/etc. + * + * @param padding + * + * Total desired length of the string. For example, setting this to `2` will ensure the string + * is at least 2 characters in size, using `c` to pad the left side where necessary. + * + * @param c + * + * The character to pad with. This defaults to `0` assuming you'll be using this function to + * create leading zeroes. + * + * @return + * + * The padded string. + */ + template + static std::string to_string_leftpad(T val, size_t padding, char c = '0') + { + std::string s = std::to_string(val); + + if (s.size() < padding) { + s.insert(0, padding - s.size(), c); + } + + return s; + } + + /** + * @brief Format a string + * + * A sprintf wrapper that returns a std::string. + * + * @param fmt + * + * The format to use. + * + * @return + * + * A formatted string in std::string form. + */ + static std::string format(const char *fmt, ...); + + // trim from start (in place) + static inline void ltrim(std::string &s) + { + s.erase(s.begin(), + std::find_if(s.begin(), s.end(), [](unsigned char ch) { + return !std::isspace(ch); + })); + } + + // trim from end (in place) + static inline void rtrim(std::string &s) + { + s.erase(std::find_if(s.rbegin(), s.rend(), + [](unsigned char ch) { return !std::isspace(ch); }) + .base(), + s.end()); + } + + // trim from both ends (in place) + static inline void trim(std::string &s) + { + rtrim(s); + ltrim(s); + } + + // trim from start (copying) + static inline std::string ltrimmed(std::string s) + { + ltrim(s); + return s; + } + + // trim from end (copying) + static inline std::string rtrimmed(std::string s) + { + rtrim(s); + return s; + } + + // trim from both ends (copying) + static inline std::string trimmed(std::string s) + { + trim(s); + return s; + } +}; + +} + +#endif // OAK_LIBOLIVECORE_STRINGUTILS_H diff --git a/core/src/oliveimpl/util/timecodefunctions.h b/core/src/oliveimpl/util/timecodefunctions.h new file mode 100644 index 000000000..d6a35a2aa --- /dev/null +++ b/core/src/oliveimpl/util/timecodefunctions.h @@ -0,0 +1,93 @@ +/*** + + Olive - Non-Linear Video Editor + Copyright (C) 2023 Olive Studios LLC + Modifications Copyright (C) 2025 mikesolar + + This program is free software: you can redistribute it and/or modify + it under the terms of the GNU General Public License as published by + the Free Software Foundation, either version 3 of the License, or + (at your option) any later version. + + This program is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU General Public License for more details. + + You should have received a copy of the GNU General Public License + along with this program. If not, see . + +***/ + +#ifndef OAK_LIBOLIVECORE_TIMECODEFUNCTIONS_H +#define OAK_LIBOLIVECORE_TIMECODEFUNCTIONS_H + +#include "rational.h" + +#include + +namespace olive::core::internal +{ + +/** + * @brief Functions for converting times/timecodes/timestamps + * + * Olive uses the following terminology through its code: + * + * `time` - time in seconds presented in a Rational form + * `timebase` - the base time unit of an audio/video stream in seconds + * `timestamp` - an integer representation of a time in timebase units (in many cases is used like a frame number) + * `timecode` a user-friendly string representation of a time according to Timecode::Display + */ +class Timecode { +public: + enum Display { + k_timecode_drop_frame, + k_timecode_non_drop_frame, + k_timecode_seconds, + k_frames, + k_milliseconds + }; + + enum Rounding { k_ceil, k_floor, k_round }; + + /** + * @brief Convert a timestamp (according to a Rational timebase) to a user-friendly string representation + */ + static std::string time_to_timecode(const Rational &time, + const Rational &timebase, + const Display &display, + bool show_plus_if_positive = false); + static Rational timecode_to_time(std::string timecode, + const Rational &timebase, + const Display &display, + bool *ok = nullptr); + + static std::string time_to_string(int64_t ms); + + static Rational snap_time_to_timebase(const Rational &time, + const Rational &timebase, + Rounding floor = k_round); + + static int64_t time_to_timestamp(const Rational &time, + const Rational &timebase, + Rounding floor = k_round); + static int64_t time_to_timestamp(const double &time, + const Rational &timebase, + Rounding floor = k_round); + + static int64_t rescale_timestamp(const int64_t &ts, const Rational &source, + const Rational &dest); + static int64_t rescale_timestamp_ceil(const int64_t &ts, + const Rational &source, + const Rational &dest); + + static Rational timestamp_to_time(const int64_t ×tamp, + const Rational &timebase); + + static bool timebase_is_drop_frame(const Rational &timebase); +}; + +} + +#endif // OAK_LIBOLIVECORE_TIMECODEFUNCTIONS_H diff --git a/core/src/oliveimpl/util/timerange.h b/core/src/oliveimpl/util/timerange.h new file mode 100644 index 000000000..10d745745 --- /dev/null +++ b/core/src/oliveimpl/util/timerange.h @@ -0,0 +1,315 @@ +/*** + + Olive - Non-Linear Video Editor + Copyright (C) 2023 Olive Studios LLC + Modifications Copyright (C) 2025 mikesolar + + This program is free software: you can redistribute it and/or modify + it under the terms of the GNU General Public License as published by + the Free Software Foundation, either version 3 of the License, or + (at your option) any later version. + + This program is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU General Public License for more details. + + You should have received a copy of the GNU General Public License + along with this program. If not, see . + +***/ + +#ifndef OAK_LIBOLIVECORE_TIMERANGE_H +#define OAK_LIBOLIVECORE_TIMERANGE_H + +#include +#include + +#include "rational.h" + +namespace olive::core::internal +{ + +class TimeRange { +public: + TimeRange() = default; + TimeRange(const Rational &in, const Rational &out); + TimeRange(const TimeRange &r) + : TimeRange(r.in(), r.out()) + { + } + + TimeRange &operator=(const TimeRange &r) + { + set_range(r.in(), r.out()); + return *this; + } + + const Rational &in() const; + const Rational &out() const; + const Rational &length() const; + + void set_in(const Rational &in); + void set_out(const Rational &out); + void set_range(const Rational &in, const Rational &out); + + bool operator==(const TimeRange &r) const; + bool operator!=(const TimeRange &r) const; + + bool overlaps_with(const TimeRange &a, bool in_inclusive = true, + bool out_inclusive = true) const; + bool contains(const TimeRange &a, bool in_inclusive = true, + bool out_inclusive = true) const; + bool contains(const Rational &r) const; + + TimeRange combined(const TimeRange &a) const; + static TimeRange combine(const TimeRange &a, const TimeRange &b); + TimeRange intersected(const TimeRange &a) const; + static TimeRange intersect(const TimeRange &a, const TimeRange &b); + + TimeRange operator+(const Rational &rhs) const; + TimeRange operator-(const Rational &rhs) const; + + const TimeRange &operator+=(const Rational &rhs); + const TimeRange &operator-=(const Rational &rhs); + + std::list split(const int &chunk_size) const; + +private: + void normalize(); + + Rational in_; + Rational out_; + Rational length_; +}; + +class TimeRangeList { +public: + TimeRangeList() = default; + + TimeRangeList(std::initializer_list r) + : array_(r) + { + } + + void insert(const TimeRangeList &list_to_add); + void insert(TimeRange range_to_add); + + void remove(const TimeRange &remove); + void remove(const TimeRangeList &list); + + template + static void util_remove(std::vector *list, const TimeRange &remove) + { + std::vector additions; + + for (auto it = list->begin(); it != list->end();) { + T &compare = *it; + + if (remove.contains(compare)) { + // This element is entirely encompassed in this range, remove it + it = list->erase(it); + } else { + if (compare.contains(remove, false, false)) { + // The remove range is within this element, only choice is to split the element into two + T new_range = compare; + new_range.set_in(remove.out()); + compare.set_out(remove.in()); + + additions.push_back(new_range); + break; + } else { + if (compare.in() < remove.in() && + compare.out() > remove.in()) { + // This element's out point overlaps the range's in, we'll trim it + compare.set_out(remove.in()); + } else if (compare.in() < remove.out() && + compare.out() > remove.out()) { + // This element's in point overlaps the range's out, we'll trim it + compare.set_in(remove.out()); + } + + it++; + } + } + } + + list->insert(list->end(), additions.begin(), additions.end()); + } + + bool contains(const TimeRange &range, bool in_inclusive = true, + bool out_inclusive = true) const; + + bool contains(const Rational &r) const + { + for (const TimeRange &range : array_) { + if (range.contains(r)) { + return true; + } + } + + return false; + } + + bool overlaps_with(const TimeRange &r, bool in_inclusive = true, + bool out_inclusive = true) const + { + for (const TimeRange &range : array_) { + if (range.overlaps_with(r, in_inclusive, out_inclusive)) { + return true; + } + } + + return false; + } + + bool isEmpty() const + { + return array_.empty(); + } + + void clear() + { + array_.clear(); + } + + int size() const + { + return array_.size(); + } + + void shift(const Rational &diff); + + void trim_in(const Rational &diff); + + void trim_out(const Rational &diff); + + TimeRangeList intersects(const TimeRange &range) const; + + using const_iterator = std::vector::const_iterator; + + const_iterator begin() const + { + return array_.cbegin(); + } + + const_iterator end() const + { + return array_.cend(); + } + + const_iterator cbegin() const + { + return begin(); + } + + const_iterator cend() const + { + return end(); + } + + const TimeRange &first() const + { + return array_.front(); + } + + const TimeRange &last() const + { + return array_.back(); + } + + const TimeRange &at(int index) const + { + return array_.at(index); + } + + const std::vector &internal_array() const + { + return array_; + } + + bool operator==(const TimeRangeList &rhs) const + { + return array_ == rhs.array_; + } + +private: + std::vector array_; +}; + +class TimeRangeListFrameIterator { +public: + TimeRangeListFrameIterator(); + TimeRangeListFrameIterator(const TimeRangeList &list, + const Rational &timebase); + + Rational snap(const Rational &r) const; + + bool get_next(Rational *out); + + bool has_next() const; + + std::vector to_vector() const + { + TimeRangeListFrameIterator copy(list_, timebase_); + std::vector times; + Rational r; + while (copy.get_next(&r)) { + times.push_back(r); + } + return times; + } + + int size(); + + void reset() + { + *this = TimeRangeListFrameIterator(); + } + + void insert(const TimeRange &range) + { + list_.insert(range); + } + + void insert(const TimeRangeList &list) + { + list_.insert(list); + } + + bool is_custom_range() const + { + return custom_range_; + } + + void set_custom_range(bool e) + { + custom_range_ = e; + } + + int frame_index() const + { + return frame_index_; + } + +private: + void update_index_if_necessary(); + + TimeRangeList list_; + + Rational timebase_; + + Rational current_; + + int range_index_; + + int size_; + + int frame_index_; + + bool custom_range_; +}; + +} + +#endif // OAK_LIBOLIVECORE_TIMERANGE_H diff --git a/core/include/olive/core/util/value.h b/core/src/oliveimpl/util/value.h similarity index 98% rename from core/include/olive/core/util/value.h rename to core/src/oliveimpl/util/value.h index e449753bf..41fd917c5 100644 --- a/core/include/olive/core/util/value.h +++ b/core/src/oliveimpl/util/value.h @@ -28,7 +28,7 @@ #include #include -namespace olive::core +namespace olive::core::internal { /** diff --git a/core/src/render/audioparams.cpp b/core/src/render/audioparams.cpp index a81a48743..4c790412d 100644 --- a/core/src/render/audioparams.cpp +++ b/core/src/render/audioparams.cpp @@ -23,7 +23,7 @@ #include -namespace olive::core +namespace olive::core::internal { const std::vector AudioParams::k_supported_sample_rates = { diff --git a/core/src/render/samplebuffer.cpp b/core/src/render/samplebuffer.cpp index 194e21755..7673198a5 100644 --- a/core/src/render/samplebuffer.cpp +++ b/core/src/render/samplebuffer.cpp @@ -29,7 +29,7 @@ #include "util/cpuoptimize.h" #include "util/log.h" -namespace olive::core +namespace olive::core::internal { SampleBuffer::SampleBuffer() diff --git a/core/src/util/bezier.cpp b/core/src/util/bezier.cpp index 2124d743a..79cd4799d 100644 --- a/core/src/util/bezier.cpp +++ b/core/src/util/bezier.cpp @@ -23,7 +23,7 @@ #include -namespace olive::core +namespace olive::core::internal { Bezier::Bezier() diff --git a/core/src/util/color.cpp b/core/src/util/color.cpp index 301ddfaf6..966bdee53 100644 --- a/core/src/util/color.cpp +++ b/core/src/util/color.cpp @@ -27,7 +27,7 @@ #include #include -namespace olive::core +namespace olive::core::internal { Color Color::from_hsv(const DataType &h, const DataType &s, const DataType &v) diff --git a/core/src/util/fractionutils.cpp b/core/src/util/fractionutils.cpp index fa16b0674..5d0f94a93 100644 --- a/core/src/util/fractionutils.cpp +++ b/core/src/util/fractionutils.cpp @@ -27,7 +27,7 @@ #include -namespace olive::core +namespace olive::core::internal { namespace diff --git a/core/src/util/rational.cpp b/core/src/util/rational.cpp index c80bab7ba..5b04c2db6 100644 --- a/core/src/util/rational.cpp +++ b/core/src/util/rational.cpp @@ -31,7 +31,7 @@ #include "util/fractionutils.h" #include "util/stringutils.h" -namespace olive::core +namespace olive::core::internal { const Rational Rational::na_n = Rational(0, 0); diff --git a/core/src/util/stringutils.cpp b/core/src/util/stringutils.cpp index b12061648..60e5152b9 100644 --- a/core/src/util/stringutils.cpp +++ b/core/src/util/stringutils.cpp @@ -24,7 +24,7 @@ #include #include -namespace olive::core +namespace olive::core::internal { std::vector StringUtils::split(const std::string &s, diff --git a/core/src/util/timecodefunctions.cpp b/core/src/util/timecodefunctions.cpp index 629dbf3cf..b7c06046f 100644 --- a/core/src/util/timecodefunctions.cpp +++ b/core/src/util/timecodefunctions.cpp @@ -27,7 +27,7 @@ #include "util/fractionutils.h" #include "util/stringutils.h" -namespace olive::core +namespace olive::core::internal { std::string Timecode::time_to_timecode(const Rational &time, diff --git a/core/src/util/timerange.cpp b/core/src/util/timerange.cpp index 864202f83..a55e90d83 100644 --- a/core/src/util/timerange.cpp +++ b/core/src/util/timerange.cpp @@ -27,7 +27,7 @@ #include "util/timecodefunctions.h" -namespace olive::core +namespace olive::core::internal { TimeRange::TimeRange(const Rational &in, const Rational &out) diff --git a/core/src/util/value.cpp b/core/src/util/value.cpp index 8e6564381..1e89218a3 100644 --- a/core/src/util/value.cpp +++ b/core/src/util/value.cpp @@ -21,7 +21,7 @@ #include "util/value.h" -namespace olive::core +namespace olive::core::internal { } diff --git a/core/tests/oakcore_audioparams_test.cpp b/core/tests/oakcore_audioparams_test.cpp new file mode 100644 index 000000000..603614470 --- /dev/null +++ b/core/tests/oakcore_audioparams_test.cpp @@ -0,0 +1,247 @@ +/*** + + 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 . + +***/ + +// Pure C API test for oakcore/audioparams.h: no gtest, no C++ wrappers. +// oakcore/audioparams.h includes oakcore/rational.h for the OakRational +// handles crossing the boundary. + +#include +#include +#include + +#include "olive/core/oakcore/audioparams.h" + +// Mirror of render/channellayout.h values, spelled out so this test only +// includes the C API header. +static const uint64_t k_layout_mono = 0x4; +static const uint64_t k_layout_stereo = 0x3; +static const uint64_t k_layout_2_1 = 0x103; +static const uint64_t k_layout_5_point1 = 0x60F; +static const uint64_t k_layout_7_point1 = 0x63F; + +// Mirror of render/sampleformat.h enum values. +static const int k_format_invalid = -1; +static const int k_format_u8_p = 0; +static const int k_format_f32_p = 4; +static const int k_format_s16 = 7; +static const int k_format_f64 = 11; + +int main() +{ + // Default constructor: invalid parameters with footage defaults + { + OakAudioParams *p = oakcore_audioparams_create_invalid(); + assert(p != nullptr); + assert(oakcore_audioparams_is_valid(p) == 0); + assert(oakcore_audioparams_sample_rate(p) == 0); + assert(oakcore_audioparams_channel_layout(p) == 0); + assert(oakcore_audioparams_channel_count(p) == 0); + assert(oakcore_audioparams_format(p) == k_format_invalid); + assert(oakcore_audioparams_bytes_per_sample_per_channel(p) == 0); + assert(oakcore_audioparams_bits_per_sample(p) == 0); + assert(oakcore_audioparams_enabled(p) == 1); + assert(oakcore_audioparams_stream_index(p) == 0); + assert(oakcore_audioparams_duration(p) == 0); + + OakRational *tb = oakcore_audioparams_time_base(p); + assert(oakcore_rational_is_null(tb) == 1); + oakcore_rational_free(tb); + + oakcore_audioparams_free(p); + } + + // Full constructor: 48000 Hz stereo s16 + OakAudioParams *p = + oakcore_audioparams_create(48000, k_layout_stereo, k_format_s16); + assert(p != nullptr); + assert(oakcore_audioparams_is_valid(p) == 1); + assert(oakcore_audioparams_sample_rate(p) == 48000); + assert(oakcore_audioparams_channel_layout(p) == k_layout_stereo); + assert(oakcore_audioparams_channel_count(p) == 2); + assert(oakcore_audioparams_format(p) == k_format_s16); + assert(oakcore_audioparams_bytes_per_sample_per_channel(p) == 2); + assert(oakcore_audioparams_bits_per_sample(p) == 16); + assert(oakcore_audioparams_enabled(p) == 1); + assert(oakcore_audioparams_stream_index(p) == 0); + assert(oakcore_audioparams_duration(p) == 0); + + // Timebase defaults to 1/sample_rate + { + OakRational *tb = oakcore_audioparams_time_base(p); + assert(oakcore_rational_numerator(tb) == 1); + assert(oakcore_rational_denominator(tb) == 48000); + oakcore_rational_free(tb); + + OakRational *srtb = oakcore_audioparams_sample_rate_as_time_base(p); + assert(oakcore_rational_numerator(srtb) == 1); + assert(oakcore_rational_denominator(srtb) == 48000); + oakcore_rational_free(srtb); + } + + // Setters / getters + oakcore_audioparams_set_sample_rate(p, 44100); + assert(oakcore_audioparams_sample_rate(p) == 44100); + oakcore_audioparams_set_sample_rate(p, 48000); + assert(oakcore_audioparams_sample_rate(p) == 48000); + + // Changing the layout recalculates the channel count + oakcore_audioparams_set_channel_layout(p, k_layout_mono); + assert(oakcore_audioparams_channel_layout(p) == k_layout_mono); + assert(oakcore_audioparams_channel_count(p) == 1); + oakcore_audioparams_set_channel_layout(p, k_layout_5_point1); + assert(oakcore_audioparams_channel_count(p) == 6); + oakcore_audioparams_set_channel_layout(p, k_layout_stereo); + assert(oakcore_audioparams_channel_count(p) == 2); + + { + OakRational *tb = oakcore_rational_create_nd(1, 1000); + oakcore_audioparams_set_time_base(p, tb); + oakcore_rational_free(tb); + + OakRational *got = oakcore_audioparams_time_base(p); + assert(oakcore_rational_numerator(got) == 1); + assert(oakcore_rational_denominator(got) == 1000); + oakcore_rational_free(got); + + // Restore the default 1/48000 timebase + tb = oakcore_rational_create_nd(1, 48000); + oakcore_audioparams_set_time_base(p, tb); + oakcore_rational_free(tb); + } + + oakcore_audioparams_set_format(p, k_format_f32_p); + assert(oakcore_audioparams_format(p) == k_format_f32_p); + assert(oakcore_audioparams_bytes_per_sample_per_channel(p) == 4); + assert(oakcore_audioparams_bits_per_sample(p) == 32); + oakcore_audioparams_set_format(p, k_format_s16); + assert(oakcore_audioparams_bytes_per_sample_per_channel(p) == 2); + + oakcore_audioparams_set_enabled(p, 0); + assert(oakcore_audioparams_enabled(p) == 0); + oakcore_audioparams_set_enabled(p, 1); + assert(oakcore_audioparams_enabled(p) == 1); + + oakcore_audioparams_set_stream_index(p, 3); + assert(oakcore_audioparams_stream_index(p) == 3); + oakcore_audioparams_set_stream_index(p, 0); + + const int64_t big_duration = int64_t(1) << 40; + oakcore_audioparams_set_duration(p, big_duration); + assert(oakcore_audioparams_duration(p) == big_duration); + oakcore_audioparams_set_duration(p, 0); + + // Time/sample/byte conversions (48000 Hz, 2 channels, 2 bytes/sample) + assert(oakcore_audioparams_time_to_samples(p, 1.0) == 48000); + assert(oakcore_audioparams_time_to_samples(p, 0.5) == 24000); + assert(oakcore_audioparams_time_to_samples(p, -1.0) == -48000); + assert(oakcore_audioparams_time_to_samples(p, 0.0) == 0); + + assert(oakcore_audioparams_samples_to_bytes_per_channel(p, 100) == 200); + assert(oakcore_audioparams_samples_to_bytes(p, 100) == 400); + assert(oakcore_audioparams_samples_to_bytes(p, 0) == 0); + + assert(oakcore_audioparams_time_to_bytes_per_channel(p, 1.0) == 96000); + assert(oakcore_audioparams_time_to_bytes(p, 1.0) == 192000); + + assert(oakcore_audioparams_bytes_to_samples(p, 400) == 100); + assert(oakcore_audioparams_bytes_to_samples(p, 0) == 0); + + // Rational-taking overloads + { + OakRational *half = oakcore_rational_create_nd(1, 2); + assert(oakcore_audioparams_time_to_samples_rational(p, half) == 24000); + assert(oakcore_audioparams_time_to_bytes_rational(p, half) == 96000); + assert(oakcore_audioparams_time_to_bytes_per_channel_rational(p, half) == + 48000); + oakcore_rational_free(half); + } + + // Rational-returning conversions + { + OakRational *t = oakcore_audioparams_samples_to_time(p, 48000); + assert(oakcore_rational_numerator(t) == 1); + assert(oakcore_rational_denominator(t) == 1); + oakcore_rational_free(t); + + t = oakcore_audioparams_samples_to_time(p, 24000); + assert(oakcore_rational_numerator(t) == 1); + assert(oakcore_rational_denominator(t) == 2); + oakcore_rational_free(t); + + t = oakcore_audioparams_bytes_to_time(p, 192000); + assert(oakcore_rational_numerator(t) == 1); + assert(oakcore_rational_denominator(t) == 1); + oakcore_rational_free(t); + + t = oakcore_audioparams_bytes_per_channel_to_time(p, 96000); + assert(oakcore_rational_numerator(t) == 1); + assert(oakcore_rational_denominator(t) == 1); + oakcore_rational_free(t); + } + + // Copy + equality + { + OakAudioParams *copy = oakcore_audioparams_copy(p); + assert(copy != nullptr); + assert(oakcore_audioparams_equals(p, copy) == 1); + assert(oakcore_audioparams_equals(copy, copy) == 1); + + oakcore_audioparams_set_sample_rate(copy, 96000); + assert(oakcore_audioparams_equals(p, copy) == 0); + oakcore_audioparams_set_sample_rate(copy, 48000); + assert(oakcore_audioparams_equals(p, copy) == 1); + + OakAudioParams *invalid = oakcore_audioparams_create_invalid(); + assert(oakcore_audioparams_equals(p, invalid) == 0); + assert(oakcore_audioparams_equals(invalid, invalid) == 1); + oakcore_audioparams_free(invalid); + + oakcore_audioparams_free(copy); + } + + // Supported channel layouts / sample rates + { + assert(oakcore_audioparams_supported_channel_layout_count() == 5); + assert(oakcore_audioparams_supported_channel_layout_at(0) == k_layout_mono); + assert(oakcore_audioparams_supported_channel_layout_at(1) == + k_layout_stereo); + assert(oakcore_audioparams_supported_channel_layout_at(2) == k_layout_2_1); + assert(oakcore_audioparams_supported_channel_layout_at(3) == + k_layout_5_point1); + assert(oakcore_audioparams_supported_channel_layout_at(4) == + k_layout_7_point1); + // Out-of-range indices return 0 + assert(oakcore_audioparams_supported_channel_layout_at(-1) == 0); + assert(oakcore_audioparams_supported_channel_layout_at(5) == 0); + + assert(oakcore_audioparams_supported_sample_rate_count() == 10); + assert(oakcore_audioparams_supported_sample_rate_at(0) == 8000); + assert(oakcore_audioparams_supported_sample_rate_at(6) == 44100); + assert(oakcore_audioparams_supported_sample_rate_at(7) == 48000); + assert(oakcore_audioparams_supported_sample_rate_at(9) == 96000); + assert(oakcore_audioparams_supported_sample_rate_at(-1) == 0); + assert(oakcore_audioparams_supported_sample_rate_at(10) == 0); + } + + oakcore_audioparams_free(p); + + std::printf("oakcore_audioparams_test: all assertions passed\n"); + return 0; +} diff --git a/core/tests/oakcore_bezier_test.cpp b/core/tests/oakcore_bezier_test.cpp new file mode 100644 index 000000000..b3c573e27 --- /dev/null +++ b/core/tests/oakcore_bezier_test.cpp @@ -0,0 +1,144 @@ +/*** + + 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 +#include +#include + +#include "olive/core/oakcore/bezier.h" + +static int double_eq(double a, double b) +{ + return fabs(a - b) < 1e-9; +} + +int main(void) +{ + // Default constructor: everything zeroed + OakBezier *b = oakcore_bezier_create(); + assert(b != NULL); + assert(double_eq(oakcore_bezier_x(b), 0.0)); + assert(double_eq(oakcore_bezier_y(b), 0.0)); + assert(double_eq(oakcore_bezier_cp1_x(b), 0.0)); + assert(double_eq(oakcore_bezier_cp1_y(b), 0.0)); + assert(double_eq(oakcore_bezier_cp2_x(b), 0.0)); + assert(double_eq(oakcore_bezier_cp2_y(b), 0.0)); + + // Setters write through to the getters + oakcore_bezier_set_x(b, 1.5); + oakcore_bezier_set_y(b, -2.25); + oakcore_bezier_set_cp1_x(b, 0.1); + oakcore_bezier_set_cp1_y(b, 0.2); + oakcore_bezier_set_cp2_x(b, 0.3); + oakcore_bezier_set_cp2_y(b, 0.4); + assert(double_eq(oakcore_bezier_x(b), 1.5)); + assert(double_eq(oakcore_bezier_y(b), -2.25)); + assert(double_eq(oakcore_bezier_cp1_x(b), 0.1)); + assert(double_eq(oakcore_bezier_cp1_y(b), 0.2)); + assert(double_eq(oakcore_bezier_cp2_x(b), 0.3)); + assert(double_eq(oakcore_bezier_cp2_y(b), 0.4)); + + // x/y constructor: control points stay zeroed + OakBezier *xy = oakcore_bezier_create_xy(3.0, 4.0); + assert(double_eq(oakcore_bezier_x(xy), 3.0)); + assert(double_eq(oakcore_bezier_y(xy), 4.0)); + assert(double_eq(oakcore_bezier_cp1_x(xy), 0.0)); + assert(double_eq(oakcore_bezier_cp1_y(xy), 0.0)); + assert(double_eq(oakcore_bezier_cp2_x(xy), 0.0)); + assert(double_eq(oakcore_bezier_cp2_y(xy), 0.0)); + + // Full constructor + OakBezier *full = + oakcore_bezier_create_full(1.0, 2.0, 0.25, 0.5, 0.75, 1.0); + assert(double_eq(oakcore_bezier_x(full), 1.0)); + assert(double_eq(oakcore_bezier_y(full), 2.0)); + assert(double_eq(oakcore_bezier_cp1_x(full), 0.25)); + assert(double_eq(oakcore_bezier_cp1_y(full), 0.5)); + assert(double_eq(oakcore_bezier_cp2_x(full), 0.75)); + assert(double_eq(oakcore_bezier_cp2_y(full), 1.0)); + + // Copy: same values, independent storage + OakBezier *dup = oakcore_bezier_copy(full); + assert(double_eq(oakcore_bezier_x(dup), 1.0)); + assert(double_eq(oakcore_bezier_cp2_y(dup), 1.0)); + oakcore_bezier_set_x(dup, 9.0); + oakcore_bezier_set_cp1_y(dup, 8.0); + assert(double_eq(oakcore_bezier_x(full), 1.0)); + assert(double_eq(oakcore_bezier_cp1_y(full), 0.5)); + + // Quadratic evaluation: endpoints and x->t->y round trip + assert(double_eq(oakcore_bezier_quadratic_tto_y(0.0, 0.5, 1.0, 0.0), 0.0)); + assert(double_eq(oakcore_bezier_quadratic_tto_y(0.0, 0.5, 1.0, 1.0), 1.0)); + { + const double x = 0.3; + const double t = + oakcore_bezier_quadratic_xto_t(x, 0.0, 0.25, 1.0); + const double y = + oakcore_bezier_quadratic_tto_y(0.0, 0.75, 1.0, t); + assert(t >= 0.0 && t <= 1.0); + // xto_t solves on the x curve; feeding the same x back must hold + assert(fabs(oakcore_bezier_quadratic_tto_y(0.0, 0.25, 1.0, t) - x) < + 1e-5); + (void) y; + } + // xto_t clamps x into [a, c] instead of diverging + { + const double t_lo = oakcore_bezier_quadratic_xto_t(-5.0, 0.0, 0.5, 1.0); + const double t_hi = oakcore_bezier_quadratic_xto_t(5.0, 0.0, 0.5, 1.0); + assert(t_lo >= 0.0 && t_lo <= 1.0); + assert(t_hi >= 0.0 && t_hi <= 1.0); + } + + // Cubic evaluation: endpoints and x->t->y round trip + assert(double_eq(oakcore_bezier_cubic_tto_y(0.0, 0.25, 0.75, 1.0, 0.0), + 0.0)); + assert(double_eq(oakcore_bezier_cubic_tto_y(0.0, 0.25, 0.75, 1.0, 1.0), + 1.0)); + { + const double x = 0.6; + const double t = + oakcore_bezier_cubic_xto_t(x, 0.0, 0.1, 0.9, 1.0); + const double y = + oakcore_bezier_cubic_tto_y(0.0, 0.8, 0.2, 1.0, t); + assert(t >= 0.0 && t <= 1.0); + assert(fabs(oakcore_bezier_cubic_tto_y(0.0, 0.1, 0.9, 1.0, t) - x) < + 1e-5); + (void) y; + } + // xto_t clamps x into [a, d] instead of diverging + { + const double t_lo = + oakcore_bezier_cubic_xto_t(-5.0, 0.0, 0.25, 0.75, 1.0); + const double t_hi = + oakcore_bezier_cubic_xto_t(5.0, 0.0, 0.25, 0.75, 1.0); + assert(t_lo >= 0.0 && t_lo <= 1.0); + assert(t_hi >= 0.0 && t_hi <= 1.0); + } + + // Ownership: every handle released exactly once + oakcore_bezier_free(b); + oakcore_bezier_free(xy); + oakcore_bezier_free(full); + oakcore_bezier_free(dup); + oakcore_bezier_free(NULL); + + printf("oakcore_bezier_test: all assertions passed\n"); + return 0; +} diff --git a/core/tests/oakcore_color_test.cpp b/core/tests/oakcore_color_test.cpp new file mode 100644 index 000000000..1a855fe4d --- /dev/null +++ b/core/tests/oakcore_color_test.cpp @@ -0,0 +1,351 @@ +/*** + + 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 . + +***/ + +/** + * @file oakcore_color_test.cpp + * @brief Pure C API test for the OakColor ABI + * + * Exercises every function of oakcore/color.h through the C boundary only: + * no C++ wrapper, no test framework, just main() + assert(). + */ + +#include "olive/core/oakcore/color.h" + +#include +#include +#include +#include +#include + +/* PixelFormat::Format values (render/pixelformat.h) */ +enum { PF_U8 = 0, PF_U10 = 1, PF_U16 = 2, PF_F16 = 3, PF_F32 = 4 }; + +static int feq(float a, float b) +{ + return fabsf(a - b) < 1e-5f; +} + +static void test_create_and_channels(void) +{ + /* Default construction: all channels zero */ + OakColor *c = oakcore_color_create(); + assert(c != NULL); + assert(feq(oakcore_color_red(c), 0.0f)); + assert(feq(oakcore_color_green(c), 0.0f)); + assert(feq(oakcore_color_blue(c), 0.0f)); + assert(feq(oakcore_color_alpha(c), 0.0f)); + oakcore_color_free(c); + + /* RGBA construction + getters */ + c = oakcore_color_create_rgba(0.25f, 0.5f, 0.75f, 1.0f); + assert(feq(oakcore_color_red(c), 0.25f)); + assert(feq(oakcore_color_green(c), 0.5f)); + assert(feq(oakcore_color_blue(c), 0.75f)); + assert(feq(oakcore_color_alpha(c), 1.0f)); + + /* Setters, including out-of-gamut HDR values */ + oakcore_color_set_red(c, -0.5f); + oakcore_color_set_green(c, 2.0f); + oakcore_color_set_blue(c, 0.0f); + oakcore_color_set_alpha(c, 0.125f); + assert(feq(oakcore_color_red(c), -0.5f)); + assert(feq(oakcore_color_green(c), 2.0f)); + assert(feq(oakcore_color_blue(c), 0.0f)); + assert(feq(oakcore_color_alpha(c), 0.125f)); + oakcore_color_free(c); +} + +static void test_copy_and_ownership(void) +{ + OakColor *c = oakcore_color_create_rgba(0.1f, 0.2f, 0.3f, 0.4f); + OakColor *copy = oakcore_color_copy(c); + assert(copy != NULL); + assert(feq(oakcore_color_red(copy), 0.1f)); + assert(feq(oakcore_color_green(copy), 0.2f)); + assert(feq(oakcore_color_blue(copy), 0.3f)); + assert(feq(oakcore_color_alpha(copy), 0.4f)); + + /* The copy is independent from the original */ + oakcore_color_set_red(copy, 0.9f); + assert(feq(oakcore_color_red(copy), 0.9f)); + assert(feq(oakcore_color_red(c), 0.1f)); + + oakcore_color_free(copy); + oakcore_color_free(c); + + /* Releasing a NULL handle must be safe */ + oakcore_color_free(NULL); +} + +static void test_hsv(void) +{ + /* Primary colors from HSV */ + OakColor *c = oakcore_color_from_hsv(0.0f, 1.0f, 1.0f); + assert(feq(oakcore_color_red(c), 1.0f)); + assert(feq(oakcore_color_green(c), 0.0f)); + assert(feq(oakcore_color_blue(c), 0.0f)); + assert(feq(oakcore_color_alpha(c), 1.0f)); + oakcore_color_free(c); + + c = oakcore_color_from_hsv(120.0f, 1.0f, 1.0f); + assert(feq(oakcore_color_red(c), 0.0f)); + assert(feq(oakcore_color_green(c), 1.0f)); + assert(feq(oakcore_color_blue(c), 0.0f)); + oakcore_color_free(c); + + c = oakcore_color_from_hsv(240.0f, 1.0f, 0.5f); + assert(feq(oakcore_color_red(c), 0.0f)); + assert(feq(oakcore_color_green(c), 0.0f)); + assert(feq(oakcore_color_blue(c), 0.5f)); + oakcore_color_free(c); + + /* to_hsv of pure red and gray */ + c = oakcore_color_create_rgba(1.0f, 0.0f, 0.0f, 1.0f); + float h = -1.0f, s = -1.0f, v = -1.0f; + oakcore_color_to_hsv(c, &h, &s, &v); + assert(feq(h, 0.0f)); + assert(feq(s, 1.0f)); + assert(feq(v, 1.0f)); + assert(feq(oakcore_color_hsv_hue(c), h)); + assert(feq(oakcore_color_hsv_saturation(c), s)); + assert(feq(oakcore_color_value(c), v)); + oakcore_color_free(c); + + c = oakcore_color_create_rgba(0.5f, 0.5f, 0.5f, 1.0f); + oakcore_color_to_hsv(c, &h, &s, &v); + assert(feq(h, 0.0f)); + assert(feq(s, 0.0f)); + assert(feq(v, 0.5f)); + oakcore_color_free(c); + + /* Roundtrip: color -> hsv -> color */ + c = oakcore_color_create_rgba(0.2f, 0.4f, 0.8f, 1.0f); + oakcore_color_to_hsv(c, &h, &s, &v); + assert(feq(h, 220.0f)); + assert(feq(s, 0.75f)); + assert(feq(v, 0.8f)); + OakColor *rt = oakcore_color_from_hsv(h, s, v); + assert(feq(oakcore_color_red(rt), 0.2f)); + assert(feq(oakcore_color_green(rt), 0.4f)); + assert(feq(oakcore_color_blue(rt), 0.8f)); + oakcore_color_free(rt); + oakcore_color_free(c); +} + +static void test_hsl(void) +{ + /* Pure red: l = 0.5, s = 1, h = 0 */ + OakColor *c = oakcore_color_create_rgba(1.0f, 0.0f, 0.0f, 1.0f); + float h = -1.0f, s = -1.0f, l = -1.0f; + oakcore_color_to_hsl(c, &h, &s, &l); + assert(feq(h, 0.0f)); + assert(feq(s, 1.0f)); + assert(feq(l, 0.5f)); + assert(feq(oakcore_color_hsl_hue(c), h)); + assert(feq(oakcore_color_hsl_saturation(c), s)); + assert(feq(oakcore_color_lightness(c), l)); + oakcore_color_free(c); + + /* Gray: zero saturation */ + c = oakcore_color_create_rgba(0.5f, 0.5f, 0.5f, 1.0f); + oakcore_color_to_hsl(c, &h, &s, &l); + assert(feq(h, 0.0f)); + assert(feq(s, 0.0f)); + assert(feq(l, 0.5f)); + oakcore_color_free(c); + + /* Arbitrary color */ + c = oakcore_color_create_rgba(0.2f, 0.4f, 0.8f, 1.0f); + oakcore_color_to_hsl(c, &h, &s, &l); + assert(feq(h, 220.0f)); + assert(feq(s, 0.6f)); + assert(feq(l, 0.5f)); + oakcore_color_free(c); +} + +static void test_data_access(void) +{ + OakColor *c = oakcore_color_create_rgba(0.1f, 0.2f, 0.3f, 0.4f); + + /* Const read access */ + const float *cd = oakcore_color_const_data(c); + assert(cd != NULL); + assert(feq(cd[0], 0.1f)); + assert(feq(cd[1], 0.2f)); + assert(feq(cd[2], 0.3f)); + assert(feq(cd[3], 0.4f)); + + /* Mutable write access */ + float *d = oakcore_color_data(c); + assert(d != NULL); + d[0] = 0.125f; + d[3] = 1.0f; + assert(feq(oakcore_color_red(c), 0.125f)); + assert(feq(oakcore_color_alpha(c), 1.0f)); + + oakcore_color_free(c); +} + +static void test_pixel_data(void) +{ + char buf[16]; + memset(buf, 0, sizeof(buf)); + + /* u8 roundtrip, 4 channels */ + OakColor *c = oakcore_color_create_rgba(1.0f, 0.0f, 1.0f, 1.0f); + oakcore_color_to_data(c, buf, PF_U8, 4); + assert((uint8_t)buf[0] == 255); + assert((uint8_t)buf[1] == 0); + assert((uint8_t)buf[2] == 255); + assert((uint8_t)buf[3] == 255); + OakColor *back = oakcore_color_from_data(buf, PF_U8, 4); + assert(feq(oakcore_color_red(back), 1.0f)); + assert(feq(oakcore_color_green(back), 0.0f)); + assert(feq(oakcore_color_blue(back), 1.0f)); + assert(feq(oakcore_color_alpha(back), 1.0f)); + oakcore_color_free(back); + + /* u8 with only 3 channels: alpha stays 0 */ + oakcore_color_to_data(c, buf, PF_U8, 3); + back = oakcore_color_from_data(buf, PF_U8, 3); + assert(feq(oakcore_color_red(back), 1.0f)); + assert(feq(oakcore_color_green(back), 0.0f)); + assert(feq(oakcore_color_blue(back), 1.0f)); + assert(feq(oakcore_color_alpha(back), 0.0f)); + oakcore_color_free(back); + + /* u16 roundtrip */ + oakcore_color_to_data(c, buf, PF_U16, 4); + assert(((uint16_t *)buf)[0] == 65535); + assert(((uint16_t *)buf)[1] == 0); + back = oakcore_color_from_data(buf, PF_U16, 4); + assert(feq(oakcore_color_red(back), 1.0f)); + assert(feq(oakcore_color_green(back), 0.0f)); + assert(feq(oakcore_color_alpha(back), 1.0f)); + oakcore_color_free(back); + oakcore_color_free(c); + + /* f32 roundtrip, exact */ + c = oakcore_color_create_rgba(0.25f, 0.5f, 0.75f, 1.0f); + oakcore_color_to_data(c, buf, PF_F32, 4); + assert(feq(((float *)buf)[0], 0.25f)); + assert(feq(((float *)buf)[3], 1.0f)); + back = oakcore_color_from_data(buf, PF_F32, 4); + assert(feq(oakcore_color_red(back), 0.25f)); + assert(feq(oakcore_color_green(back), 0.5f)); + assert(feq(oakcore_color_blue(back), 0.75f)); + assert(feq(oakcore_color_alpha(back), 1.0f)); + oakcore_color_free(back); + + /* f16 roundtrip: 1.0 = 0x3C00, 0.5 = 0x3800 in IEEE half */ + c = oakcore_color_create_rgba(1.0f, 0.5f, 0.0f, 1.0f); + oakcore_color_to_data(c, buf, PF_F16, 4); + assert(((uint16_t *)buf)[0] == 0x3C00); + assert(((uint16_t *)buf)[1] == 0x3800); + back = oakcore_color_from_data(buf, PF_F16, 4); + assert(feq(oakcore_color_red(back), 1.0f)); + assert(feq(oakcore_color_green(back), 0.5f)); + assert(feq(oakcore_color_blue(back), 0.0f)); + assert(feq(oakcore_color_alpha(back), 1.0f)); + oakcore_color_free(back); + + /* u10 packed 4-channel roundtrip: all ones packs to 0xFFFFFFFF */ + c = oakcore_color_create_rgba(1.0f, 1.0f, 1.0f, 1.0f); + oakcore_color_to_data(c, buf, PF_U10, 4); + assert(((uint32_t *)buf)[0] == 0xFFFFFFFFu); + back = oakcore_color_from_data(buf, PF_U10, 4); + assert(feq(oakcore_color_red(back), 1.0f)); + assert(feq(oakcore_color_green(back), 1.0f)); + assert(feq(oakcore_color_blue(back), 1.0f)); + assert(feq(oakcore_color_alpha(back), 1.0f)); + oakcore_color_free(back); + oakcore_color_free(c); +} + +static void test_luminance(void) +{ + /* (2r + b + 3g) / 6 */ + OakColor *c = oakcore_color_create_rgba(1.0f, 1.0f, 1.0f, 1.0f); + assert(feq(oakcore_color_get_rough_luminance(c), 1.0f)); + oakcore_color_free(c); + + c = oakcore_color_create_rgba(0.5f, 0.5f, 0.5f, 1.0f); + assert(feq(oakcore_color_get_rough_luminance(c), 0.5f)); + oakcore_color_free(c); + + c = oakcore_color_create_rgba(0.6f, 0.2f, 0.4f, 1.0f); + assert(feq(oakcore_color_get_rough_luminance(c), + (2.0f * 0.6f + 0.4f + 3.0f * 0.2f) / 6.0f)); + oakcore_color_free(c); +} + +static void test_math(void) +{ + OakColor *a = oakcore_color_create_rgba(0.1f, 0.2f, 0.3f, 0.4f); + OakColor *b = oakcore_color_create_rgba(0.4f, 0.3f, 0.2f, 0.1f); + + /* Color +=/-= Color */ + oakcore_color_add_assign(a, b); + assert(feq(oakcore_color_red(a), 0.5f)); + assert(feq(oakcore_color_green(a), 0.5f)); + assert(feq(oakcore_color_blue(a), 0.5f)); + assert(feq(oakcore_color_alpha(a), 0.5f)); + oakcore_color_sub_assign(a, b); + assert(feq(oakcore_color_red(a), 0.1f)); + assert(feq(oakcore_color_green(a), 0.2f)); + assert(feq(oakcore_color_blue(a), 0.3f)); + assert(feq(oakcore_color_alpha(a), 0.4f)); + + /* Scalar assign operators (apply to all channels) */ + oakcore_color_add_scalar_assign(a, 1.0f); + assert(feq(oakcore_color_red(a), 1.1f)); + assert(feq(oakcore_color_alpha(a), 1.4f)); + oakcore_color_sub_scalar_assign(a, 0.6f); + assert(feq(oakcore_color_red(a), 0.5f)); + assert(feq(oakcore_color_alpha(a), 0.8f)); + oakcore_color_mul_scalar_assign(a, 2.0f); + assert(feq(oakcore_color_red(a), 1.0f)); + assert(feq(oakcore_color_green(a), 1.2f)); + assert(feq(oakcore_color_blue(a), 1.4f)); + assert(feq(oakcore_color_alpha(a), 1.6f)); + oakcore_color_div_scalar_assign(a, 4.0f); + assert(feq(oakcore_color_red(a), 0.25f)); + assert(feq(oakcore_color_green(a), 0.3f)); + assert(feq(oakcore_color_blue(a), 0.35f)); + assert(feq(oakcore_color_alpha(a), 0.4f)); + + oakcore_color_free(b); + oakcore_color_free(a); +} + +int main(void) +{ + test_create_and_channels(); + test_copy_and_ownership(); + test_hsv(); + test_hsl(); + test_data_access(); + test_pixel_data(); + test_luminance(); + test_math(); + + printf("oakcore_color_test: all assertions passed\n"); + return 0; +} diff --git a/core/tests/oakcore_fractionutils_test.cpp b/core/tests/oakcore_fractionutils_test.cpp new file mode 100644 index 000000000..1ec3cb117 --- /dev/null +++ b/core/tests/oakcore_fractionutils_test.cpp @@ -0,0 +1,126 @@ +/*** + + 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 "oakcore/fractionutils.h" + +#include +#include +#include +#include +#include + +/** + * @file oakcore_fractionutils_test.cpp + * @brief Pure C API test for the fraction utility functions + * + * Exercises every oakcore_fractionutils_* function directly, without the + * C++ wrapper and without a test framework. + */ +int main() +{ + // reduce_fraction: greatest common divisor is divided out + { + int64_t num = 6, den = 9; + oakcore_fractionutils_reduce_fraction(&num, &den, INT64_MAX); + assert(num == 2 && den == 3); + } + + // reduce_fraction: the sign is normalized into the numerator + { + int64_t num = -6, den = 9; + oakcore_fractionutils_reduce_fraction(&num, &den, INT64_MAX); + assert(num == -2 && den == 3); + } + { + int64_t num = 6, den = -9; + oakcore_fractionutils_reduce_fraction(&num, &den, INT64_MAX); + assert(num == -2 && den == 3); + } + + // reduce_fraction: a zero denominator is preserved, numerator zeroed + { + int64_t num = 42, den = 0; + oakcore_fractionutils_reduce_fraction(&num, &den, INT64_MAX); + assert(num == 0 && den == 0); + } + + // reduce_fraction: a zero numerator reduces to 0/1 + { + int64_t num = 0, den = 7; + oakcore_fractionutils_reduce_fraction(&num, &den, INT64_MAX); + assert(num == 0 && den == 1); + } + + // reduce_fraction: exact reduction well within max + { + int64_t num = 1000000, den = 1000000; + oakcore_fractionutils_reduce_fraction(&num, &den, 100); + assert(num == 1 && den == 1); + } + + // reduce_fraction: continued-fraction approximation respects max + { + int64_t num = 1048576, den = 1000000; + oakcore_fractionutils_reduce_fraction(&num, &den, 10000); + assert(num > 0 && num <= 10000 && den > 0 && den <= 10000); + const double approx = double(num) / double(den); + assert(fabs(approx - 1.048576) < 0.001); + } + + // compare_fractions: three-way result -1 / 0 / 1 + assert(oakcore_fractionutils_compare_fractions(1, 3, 1, 2) == -1); + assert(oakcore_fractionutils_compare_fractions(1, 2, 2, 4) == 0); + assert(oakcore_fractionutils_compare_fractions(2, 3, 1, 2) == 1); + + // compare_fractions: meaningless degenerate comparison returns INT_MIN + assert(oakcore_fractionutils_compare_fractions(0, 0, 0, 5) == INT_MIN); + + // rescale_rnd: exact division needs no rounding + assert(oakcore_fractionutils_rescale_rnd(100, 3, 4, + OAK_FRACTION_ROUNDING_NEAR_INF) == 75); + + // rescale_rnd: round to nearest, halfway cases away from zero + assert(oakcore_fractionutils_rescale_rnd(5, 1, 2, + OAK_FRACTION_ROUNDING_NEAR_INF) == 3); + assert(oakcore_fractionutils_rescale_rnd(-5, 1, 2, + OAK_FRACTION_ROUNDING_NEAR_INF) == -3); + assert(oakcore_fractionutils_rescale_rnd(7, 1, 2, + OAK_FRACTION_ROUNDING_NEAR_INF) == 4); + + // rescale_rnd: round toward positive infinity + assert(oakcore_fractionutils_rescale_rnd(5, 1, 2, + OAK_FRACTION_ROUNDING_UP) == 3); + assert(oakcore_fractionutils_rescale_rnd(-5, 1, 2, + OAK_FRACTION_ROUNDING_UP) == -2); + assert(oakcore_fractionutils_rescale_rnd(4, 1, 2, + OAK_FRACTION_ROUNDING_UP) == 2); + + // rescale_rnd: a negative divisor is normalized into the multiplier + assert(oakcore_fractionutils_rescale_rnd(10, 1, -2, + OAK_FRACTION_ROUNDING_NEAR_INF) == -5); + + // rescale_rnd: large intermediates stay exact (128-bit path) + assert(oakcore_fractionutils_rescale_rnd(INT64_MAX / 2, 4, 2, + OAK_FRACTION_ROUNDING_NEAR_INF) + == INT64_MAX - 1); + + printf("oakcore_fractionutils_test: all assertions passed\n"); + return 0; +} diff --git a/core/tests/oakcore_rational_test.cpp b/core/tests/oakcore_rational_test.cpp new file mode 100644 index 000000000..c9c7491e6 --- /dev/null +++ b/core/tests/oakcore_rational_test.cpp @@ -0,0 +1,110 @@ +#include +#include +#include +#include + +#include "olive/core/oakcore/rational.h" + +int main() +{ + // create_nd reduces and normalizes signs + OakRational *r = oakcore_rational_create_nd(2, 4); + assert(r != NULL); + assert(oakcore_rational_numerator(r) == 1); + assert(oakcore_rational_denominator(r) == 2); + + OakRational *neg = oakcore_rational_create_nd(1, -2); + assert(oakcore_rational_numerator(neg) == -1); + assert(oakcore_rational_denominator(neg) == 2); + oakcore_rational_free(neg); + + // create defaults to n/1 + OakRational *five = oakcore_rational_create(5); + assert(oakcore_rational_numerator(five) == 5); + assert(oakcore_rational_denominator(five) == 1); + oakcore_rational_free(five); + + // NaN + OakRational *nan = oakcore_rational_create_nan(); + assert(oakcore_rational_is_nan(nan) == 1); + assert(oakcore_rational_is_null(nan) == 1); + assert(isnan(oakcore_rational_to_double(nan))); + oakcore_rational_free(nan); + + // copy is deep + OakRational *copy = oakcore_rational_copy(r); + assert(copy != r); + assert(oakcore_rational_compare(copy, r) == 0); + oakcore_rational_add_assign(copy, r); + assert(oakcore_rational_numerator(copy) == 1); + assert(oakcore_rational_denominator(copy) == 1); + assert(oakcore_rational_numerator(r) == 1); + assert(oakcore_rational_denominator(r) == 2); + oakcore_rational_free(copy); + + // to_double / to_string + assert(fabs(oakcore_rational_to_double(r) - 0.5) < 1e-12); + char buf[32]; + int needed = oakcore_rational_to_string(r, NULL, 0); + assert(needed == 3); + assert(oakcore_rational_to_string(r, buf, sizeof(buf)) == 3); + assert(strcmp(buf, "1/2") == 0); + assert(oakcore_rational_to_string(r, buf, 2) == 3); // truncated write + assert(strcmp(buf, "1") == 0); + + // from_double / from_string + int ok = 0; + OakRational *half = oakcore_rational_from_double(0.5, &ok); + assert(ok == 1); + assert(oakcore_rational_compare(half, r) == 0); + oakcore_rational_free(half); + + OakRational *bad = oakcore_rational_from_double(NAN, &ok); + assert(ok == 0); + assert(oakcore_rational_is_nan(bad)); + oakcore_rational_free(bad); + + OakRational *parsed = oakcore_rational_from_string("3/4", &ok); + assert(ok == 1); + assert(fabs(oakcore_rational_to_double(parsed) - 0.75) < 1e-12); + oakcore_rational_free(parsed); + + OakRational *unparsed = oakcore_rational_from_string("1/2/3", &ok); + assert(ok == 0); + oakcore_rational_free(unparsed); + + // flip + OakRational *flip = oakcore_rational_flipped(r); + assert(oakcore_rational_numerator(flip) == 2); + assert(oakcore_rational_denominator(flip) == 1); + oakcore_rational_flip(flip); + assert(oakcore_rational_compare(flip, r) == 0); + oakcore_rational_free(flip); + + // arithmetic assignments + OakRational *a = oakcore_rational_create_nd(1, 3); + OakRational *b = oakcore_rational_create_nd(1, 6); + oakcore_rational_add_assign(a, b); + assert(oakcore_rational_numerator(a) == 1); + assert(oakcore_rational_denominator(a) == 2); + oakcore_rational_sub_assign(a, b); + assert(oakcore_rational_numerator(a) == 1); + assert(oakcore_rational_denominator(a) == 3); + oakcore_rational_mul_assign(a, b); + assert(oakcore_rational_numerator(a) == 1); + assert(oakcore_rational_denominator(a) == 18); + oakcore_rational_div_assign(a, b); + assert(oakcore_rational_numerator(a) == 1); + assert(oakcore_rational_denominator(a) == 3); + + // compare ordering + assert(oakcore_rational_compare(b, a) < 0); + assert(oakcore_rational_compare(a, b) > 0); + + oakcore_rational_free(a); + oakcore_rational_free(b); + oakcore_rational_free(r); + + printf("oakcore_rational_test: all assertions passed\n"); + return 0; +} diff --git a/core/tests/oakcore_samplebuffer_test.cpp b/core/tests/oakcore_samplebuffer_test.cpp new file mode 100644 index 000000000..195648459 --- /dev/null +++ b/core/tests/oakcore_samplebuffer_test.cpp @@ -0,0 +1,359 @@ +/*** + + 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 +#include +#include +#include + +#include "olive/core/oakcore/samplebuffer.h" + +/* These functions are declared in oakcore/audioparams.h (a parallel + * delivery); they are re-declared here so that this test only includes the + * samplebuffer C header it is meant to exercise. */ +extern "C" { +OakAudioParams *oakcore_audioparams_create(int sample_rate, + uint64_t channel_layout, + int format); +void oakcore_audioparams_free(OakAudioParams *self); +int oakcore_audioparams_sample_rate(const OakAudioParams *self); +int oakcore_audioparams_channel_count(const OakAudioParams *self); +} + +/* Values mirror render/channellayout.h (k_channel_layout_stereo) and + * render/sampleformat.h (SampleFormat::f32_p) on the library side. */ +#define SAMPLE_RATE 48000 +#define CHANNEL_LAYOUT_STEREO 0x3 +#define FORMAT_F32P 4 + +static int float_eq(float a, float b) +{ + return fabsf(a - b) < 1e-6f; +} + +int main(void) +{ + /* --- Default construction: empty, unallocated --- */ + OakSampleBuffer *def = oakcore_samplebuffer_create(); + assert(def != NULL); + assert(oakcore_samplebuffer_is_allocated(def) == 0); + assert(oakcore_samplebuffer_sample_count(def) == 0); + assert(oakcore_samplebuffer_channel_count(def) == 0); + assert(oakcore_samplebuffer_data(def, 0) == NULL); + + /* audio_params() hands back an owned copy of the (invalid) defaults */ + OakAudioParams *def_params = oakcore_samplebuffer_audio_params(def); + assert(def_params != NULL); + assert(oakcore_audioparams_sample_rate(def_params) == 0); + oakcore_audioparams_free(def_params); + + /* Operations that need an allocation warn and leave the buffer alone */ + oakcore_samplebuffer_allocate(def); + assert(oakcore_samplebuffer_is_allocated(def) == 0); + oakcore_samplebuffer_silence(def); + oakcore_samplebuffer_reverse(def); + assert(oakcore_samplebuffer_is_allocated(def) == 0); + oakcore_samplebuffer_free(def); + + /* --- Construction from audio params + sample count --- */ + OakAudioParams *stereo = oakcore_audioparams_create( + SAMPLE_RATE, CHANNEL_LAYOUT_STEREO, FORMAT_F32P); + assert(stereo != NULL); + + OakSampleBuffer *buf = oakcore_samplebuffer_create_samples(stereo, 100); + assert(oakcore_samplebuffer_is_allocated(buf) == 1); + assert(oakcore_samplebuffer_sample_count(buf) == 100); + assert(oakcore_samplebuffer_channel_count(buf) == 2); + + /* A fresh allocation is silent */ + for (int ch = 0; ch < 2; ch++) { + const float *d = oakcore_samplebuffer_data(buf, ch); + assert(d != NULL); + for (int i = 0; i < 100; i++) { + assert(float_eq(d[i], 0.0f)); + } + } + + /* audio_params() copy reflects the construction parameters */ + OakAudioParams *p = oakcore_samplebuffer_audio_params(buf); + assert(oakcore_audioparams_sample_rate(p) == SAMPLE_RATE); + assert(oakcore_audioparams_channel_count(p) == 2); + oakcore_audioparams_free(p); + + /* data() rejects out-of-range channels */ + assert(oakcore_samplebuffer_data(buf, -1) == NULL); + assert(oakcore_samplebuffer_data(buf, 2) == NULL); + + /* --- Construction from audio params + rational length (0.5s) --- */ + OakRational *half_sec = oakcore_rational_create_nd(1, 2); + OakSampleBuffer *timed = + oakcore_samplebuffer_create_length(stereo, half_sec); + assert(oakcore_samplebuffer_is_allocated(timed) == 1); + assert(oakcore_samplebuffer_sample_count(timed) == 24000); + oakcore_rational_free(half_sec); + oakcore_samplebuffer_free(timed); + + /* --- Manual lifecycle: params + count + allocate, destroy, repeat --- */ + OakSampleBuffer *manual = oakcore_samplebuffer_create(); + oakcore_samplebuffer_set_audio_params(manual, stereo); + oakcore_samplebuffer_set_sample_count(manual, 50); + oakcore_samplebuffer_allocate(manual); + assert(oakcore_samplebuffer_is_allocated(manual) == 1); + assert(oakcore_samplebuffer_sample_count(manual) == 50); + assert(oakcore_samplebuffer_channel_count(manual) == 2); + + oakcore_samplebuffer_destroy(manual); + assert(oakcore_samplebuffer_is_allocated(manual) == 0); + assert(oakcore_samplebuffer_channel_count(manual) == 0); + assert(oakcore_samplebuffer_data(manual, 0) == NULL); + + oakcore_samplebuffer_set_sample_count(manual, 30); + oakcore_samplebuffer_allocate(manual); + assert(oakcore_samplebuffer_is_allocated(manual) == 1); + assert(oakcore_samplebuffer_sample_count(manual) == 30); + oakcore_samplebuffer_free(manual); + + /* set_sample_count via a rational length: 0.001s at 48kHz = 48 samples */ + OakSampleBuffer *manual2 = oakcore_samplebuffer_create(); + oakcore_samplebuffer_set_audio_params(manual2, stereo); + OakRational *one_ms = oakcore_rational_create_nd(1, 1000); + oakcore_samplebuffer_set_sample_count_length(manual2, one_ms); + oakcore_rational_free(one_ms); + oakcore_samplebuffer_allocate(manual2); + assert(oakcore_samplebuffer_sample_count(manual2) == 48); + oakcore_samplebuffer_free(manual2); + + /* --- data() direct access, set(), to_raw_ptrs() --- */ + float *ch0 = oakcore_samplebuffer_data(buf, 0); + float *ch1 = oakcore_samplebuffer_data(buf, 1); + for (int i = 0; i < 100; i++) { + ch0[i] = (float)i; /* ramp 0..99 on channel 0 */ + ch1[i] = (float)(i * 10); /* ramp 0..990 on channel 1 */ + } + + const float ins[4] = { -1.0f, -2.0f, -3.0f, -4.0f }; + oakcore_samplebuffer_set(buf, 0, ins, 10, 4); /* write at offset 10 */ + for (int k = 0; k < 4; k++) { + assert(float_eq(ch0[10 + k], ins[k])); + } + assert(float_eq(ch0[9], 9.0f)); /* neighbours untouched */ + assert(float_eq(ch0[14], 14.0f)); + + oakcore_samplebuffer_set(buf, 1, ins, 0, 4); /* write at offset 0 */ + for (int k = 0; k < 4; k++) { + assert(float_eq(ch1[k], ins[k])); + } + for (int k = 0; k < 4; k++) { /* restore the channel 1 ramp */ + ch1[k] = (float)(k * 10); + } + + float *ptrs[2] = { NULL, NULL }; + oakcore_samplebuffer_to_raw_ptrs(buf, ptrs); + assert(ptrs[0] == ch0); + assert(ptrs[1] == ch1); + ptrs[1][50] = 123.0f; /* writes through the raw pointer land in the buffer */ + assert(float_eq(oakcore_samplebuffer_data(buf, 1)[50], 123.0f)); + ch1[50] = 500.0f; /* restore ramp value (50 * 10) */ + + /* --- copy: an independent deep copy --- */ + OakSampleBuffer *cp = oakcore_samplebuffer_copy(buf); + assert(oakcore_samplebuffer_is_allocated(cp) == 1); + assert(oakcore_samplebuffer_sample_count(cp) == 100); + assert(oakcore_samplebuffer_channel_count(cp) == 2); + assert(float_eq(oakcore_samplebuffer_data(cp, 0)[9], 9.0f)); + + /* --- transform_volume (in place, all channels) --- */ + oakcore_samplebuffer_transform_volume(cp, 2.0f); + assert(float_eq(oakcore_samplebuffer_data(cp, 0)[9], 18.0f)); + assert(float_eq(oakcore_samplebuffer_data(cp, 1)[9], 180.0f)); + assert(float_eq(oakcore_samplebuffer_data(buf, 0)[9], 9.0f)); /* source keeps its data */ + + /* --- transform_volume_for_channel (in place, one channel) --- */ + oakcore_samplebuffer_transform_volume_for_channel(cp, 1, 0.5f); + assert(float_eq(oakcore_samplebuffer_data(cp, 1)[9], 90.0f)); + assert(float_eq(oakcore_samplebuffer_data(cp, 0)[9], 18.0f)); /* other channel untouched */ + oakcore_samplebuffer_free(cp); + + /* --- static-style transforms: input -> output --- */ + OakSampleBuffer *tin = oakcore_samplebuffer_create_samples(stereo, 10); + OakSampleBuffer *tout = oakcore_samplebuffer_create_samples(stereo, 10); + for (int i = 0; i < 10; i++) { + oakcore_samplebuffer_data(tin, 0)[i] = (float)i; + oakcore_samplebuffer_data(tin, 1)[i] = (float)(100 + i); + } + + oakcore_samplebuffer_transform_volume_to(0.5f, tin, tout); + for (int i = 0; i < 10; i++) { + assert(float_eq(oakcore_samplebuffer_data(tout, 0)[i], (float)i * 0.5f)); + assert(float_eq(oakcore_samplebuffer_data(tout, 1)[i], + (float)(100 + i) * 0.5f)); + assert(float_eq(oakcore_samplebuffer_data(tin, 0)[i], (float)i)); /* input unchanged */ + } + + oakcore_samplebuffer_silence(tout); + oakcore_samplebuffer_transform_volume_for_channel_to(1, 2.0f, tin, tout); + for (int i = 0; i < 10; i++) { + assert(float_eq(oakcore_samplebuffer_data(tout, 1)[i], + (float)(100 + i) * 2.0f)); + assert(float_eq(oakcore_samplebuffer_data(tout, 0)[i], 0.0f)); /* channel 0 untouched */ + } + + /* --- per-sample volume transforms --- */ + oakcore_samplebuffer_transform_volume_for_sample(tin, 3, 10.0f); + assert(float_eq(oakcore_samplebuffer_data(tin, 0)[3], 30.0f)); + assert(float_eq(oakcore_samplebuffer_data(tin, 1)[3], 1030.0f)); + + oakcore_samplebuffer_transform_volume_for_sample_on_channel(tin, 4, 0, + 100.0f); + assert(float_eq(oakcore_samplebuffer_data(tin, 0)[4], 400.0f)); + assert(float_eq(oakcore_samplebuffer_data(tin, 1)[4], 104.0f)); /* channel 1 untouched */ + oakcore_samplebuffer_free(tin); + oakcore_samplebuffer_free(tout); + + /* --- clamp() limits every channel to [-1, 1] --- */ + OakSampleBuffer *cl = oakcore_samplebuffer_create_samples(stereo, 4); + oakcore_samplebuffer_data(cl, 0)[0] = 2.5f; + oakcore_samplebuffer_data(cl, 0)[1] = -2.5f; + oakcore_samplebuffer_data(cl, 1)[0] = 42.0f; + oakcore_samplebuffer_data(cl, 1)[1] = 0.25f; + oakcore_samplebuffer_clamp(cl); + assert(float_eq(oakcore_samplebuffer_data(cl, 0)[0], 1.0f)); + assert(float_eq(oakcore_samplebuffer_data(cl, 0)[1], -1.0f)); + assert(float_eq(oakcore_samplebuffer_data(cl, 1)[0], 1.0f)); + assert(float_eq(oakcore_samplebuffer_data(cl, 1)[1], 0.25f)); + oakcore_samplebuffer_free(cl); + + /* --- silence / silence_range / silence_bytes --- */ + OakSampleBuffer *si = oakcore_samplebuffer_create_samples(stereo, 10); + for (int ch = 0; ch < 2; ch++) { + for (int i = 0; i < 10; i++) { + oakcore_samplebuffer_data(si, ch)[i] = 1.0f; + } + } + + oakcore_samplebuffer_silence_range(si, 2, 5); /* samples [2, 5) */ + for (int ch = 0; ch < 2; ch++) { + const float *d = oakcore_samplebuffer_data(si, ch); + assert(float_eq(d[1], 1.0f)); + assert(float_eq(d[2], 0.0f)); + assert(float_eq(d[4], 0.0f)); + assert(float_eq(d[5], 1.0f)); + } + + oakcore_samplebuffer_silence_bytes(si, 0, 2 * sizeof(float)); /* samples [0, 2) */ + for (int ch = 0; ch < 2; ch++) { + const float *d = oakcore_samplebuffer_data(si, ch); + assert(float_eq(d[0], 0.0f)); + assert(float_eq(d[1], 0.0f)); + assert(float_eq(d[5], 1.0f)); + } + + oakcore_samplebuffer_silence(si); /* everything */ + for (int ch = 0; ch < 2; ch++) { + const float *d = oakcore_samplebuffer_data(si, ch); + for (int i = 0; i < 10; i++) { + assert(float_eq(d[i], 0.0f)); + } + } + oakcore_samplebuffer_free(si); + + /* --- reverse() flips every channel --- */ + OakSampleBuffer *rv = oakcore_samplebuffer_create_samples(stereo, 5); + for (int i = 0; i < 5; i++) { + oakcore_samplebuffer_data(rv, 0)[i] = (float)i; + oakcore_samplebuffer_data(rv, 1)[i] = (float)(10 * i); + } + oakcore_samplebuffer_reverse(rv); + for (int i = 0; i < 5; i++) { + assert(float_eq(oakcore_samplebuffer_data(rv, 0)[i], (float)(4 - i))); + assert(float_eq(oakcore_samplebuffer_data(rv, 1)[i], + (float)(10 * (4 - i)))); + } + oakcore_samplebuffer_free(rv); + + /* --- speed(2.0) halves the sample count, sampling the ramp exactly --- */ + OakSampleBuffer *sp = oakcore_samplebuffer_create_samples(stereo, 100); + for (int i = 0; i < 100; i++) { + oakcore_samplebuffer_data(sp, 0)[i] = (float)i; + } + oakcore_samplebuffer_speed(sp, 2.0); + assert(oakcore_samplebuffer_sample_count(sp) == 50); + for (int i = 0; i < 50; i++) { + assert(float_eq(oakcore_samplebuffer_data(sp, 0)[i], (float)(2 * i))); + } + oakcore_samplebuffer_free(sp); + + /* --- fast_set(): channel copy between buffers --- */ + OakSampleBuffer *fa = oakcore_samplebuffer_create_samples(stereo, 10); + OakSampleBuffer *fb = oakcore_samplebuffer_create_samples(stereo, 10); + for (int i = 0; i < 10; i++) { + oakcore_samplebuffer_data(fa, 0)[i] = (float)i; + oakcore_samplebuffer_data(fa, 1)[i] = (float)(100 + i); + } + + oakcore_samplebuffer_fast_set(fb, fa, 1, 0); /* fb channel 1 <- fa channel 0 */ + for (int i = 0; i < 10; i++) { + assert(float_eq(oakcore_samplebuffer_data(fb, 1)[i], (float)i)); + assert(float_eq(oakcore_samplebuffer_data(fb, 0)[i], 0.0f)); + } + + oakcore_samplebuffer_fast_set(fb, fa, 0, -1); /* from == -1 mirrors to */ + for (int i = 0; i < 10; i++) { + assert(float_eq(oakcore_samplebuffer_data(fb, 0)[i], (float)i)); + } + oakcore_samplebuffer_free(fa); + oakcore_samplebuffer_free(fb); + + /* --- rip_channel(): a new mono buffer with one channel's samples --- */ + OakSampleBuffer *rip = oakcore_samplebuffer_rip_channel(buf, 1); + assert(oakcore_samplebuffer_is_allocated(rip) == 1); + assert(oakcore_samplebuffer_channel_count(rip) == 1); + assert(oakcore_samplebuffer_sample_count(rip) == 100); + for (int i = 0; i < 100; i++) { + assert(float_eq(oakcore_samplebuffer_data(rip, 0)[i], (float)(i * 10))); + } + OakAudioParams *rip_params = oakcore_samplebuffer_audio_params(rip); + assert(oakcore_audioparams_sample_rate(rip_params) == SAMPLE_RATE); + assert(oakcore_audioparams_channel_count(rip_params) == 1); + oakcore_audioparams_free(rip_params); + oakcore_samplebuffer_free(rip); + + /* --- rip_channel_vector(): query size, partial copy, full copy --- */ + assert(oakcore_samplebuffer_rip_channel_vector(buf, 1, NULL, 0) == 100); + + float partial[4] = { 0.0f, 0.0f, 0.0f, 0.0f }; + assert(oakcore_samplebuffer_rip_channel_vector(buf, 1, partial, 4) == 100); + for (int k = 0; k < 4; k++) { + assert(float_eq(partial[k], (float)(k * 10))); + } + + float full[100]; + assert(oakcore_samplebuffer_rip_channel_vector(buf, 1, full, 100) == 100); + for (int i = 0; i < 100; i++) { + assert(float_eq(full[i], (float)(i * 10))); + } + + /* --- Ownership: everything released --- */ + oakcore_samplebuffer_free(buf); + oakcore_audioparams_free(stereo); + + printf("oakcore_samplebuffer_test: all assertions passed\n"); + return 0; +} diff --git a/core/tests/oakcore_stringutils_test.cpp b/core/tests/oakcore_stringutils_test.cpp new file mode 100644 index 000000000..37e7df936 --- /dev/null +++ b/core/tests/oakcore_stringutils_test.cpp @@ -0,0 +1,174 @@ +/*** + + 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 . + +***/ + +/** + * @file oakcore_stringutils_test.cpp + * @brief Pure C API test for oakcore/stringutils.h + * + * Exercises every C function directly (no C++ wrapper, no test framework). + */ + +#include +#include +#include +#include + +#include "olive/core/oakcore/stringutils.h" + +static void test_split() +{ + int count = 0; + + // Basic split + char **arr = oakcore_stringutils_split("a,b,c", ',', &count); + assert(arr != NULL); + assert(count == 3); + assert(strcmp(arr[0], "a") == 0); + assert(strcmp(arr[1], "b") == 0); + assert(strcmp(arr[2], "c") == 0); + oakcore_stringutils_free_string_array(arr, count); + + // No separator present: single element equal to the whole string + arr = oakcore_stringutils_split("abc", ',', &count); + assert(arr != NULL); + assert(count == 1); + assert(strcmp(arr[0], "abc") == 0); + oakcore_stringutils_free_string_array(arr, count); + + // Trailing separator yields a trailing empty string + arr = oakcore_stringutils_split("a,", ',', &count); + assert(arr != NULL); + assert(count == 2); + assert(strcmp(arr[0], "a") == 0); + assert(strcmp(arr[1], "") == 0); + oakcore_stringutils_free_string_array(arr, count); + + // Empty string yields one empty string + arr = oakcore_stringutils_split("", ',', &count); + assert(arr != NULL); + assert(count == 1); + assert(strcmp(arr[0], "") == 0); + oakcore_stringutils_free_string_array(arr, count); + + // NULL string behaves like an empty string + arr = oakcore_stringutils_split(NULL, ',', &count); + assert(arr != NULL); + assert(count == 1); + assert(strcmp(arr[0], "") == 0); + oakcore_stringutils_free_string_array(arr, count); +} + +static void test_split_regex() +{ + int count = 0; + + // Split on runs of digits + char **arr = oakcore_stringutils_split_regex("a1b22c", "[0-9]+", &count); + assert(arr != NULL); + assert(count == 3); + assert(strcmp(arr[0], "a") == 0); + assert(strcmp(arr[1], "b") == 0); + assert(strcmp(arr[2], "c") == 0); + oakcore_stringutils_free_string_array(arr, count); + + // Pattern that never matches yields the whole string + arr = oakcore_stringutils_split_regex("abc", "[0-9]+", &count); + assert(arr != NULL); + assert(count == 1); + assert(strcmp(arr[0], "abc") == 0); + oakcore_stringutils_free_string_array(arr, count); + + // Freeing NULL is safe + oakcore_stringutils_free_string_array(NULL, 0); +} + +static void test_to_int() +{ + int ok = 0; + + // Base 10 + assert(oakcore_stringutils_to_int("42", 10, &ok) == 42); + assert(ok == 1); + + // Negative + assert(oakcore_stringutils_to_int("-17", 10, &ok) == -17); + assert(ok == 1); + + // Base 16 + assert(oakcore_stringutils_to_int("ff", 16, &ok) == 255); + assert(ok == 1); + + // Parser error: returns 0 and reports failure + assert(oakcore_stringutils_to_int("xyz", 10, &ok) == 0); + assert(ok == 0); + + // ok output parameter is optional + assert(oakcore_stringutils_to_int("7", 10, NULL) == 7); +} + +/* Variadic forwarder to exercise oakcore_stringutils_format_v() */ +static int format_forward(char *buf, int buf_size, const char *fmt, ...) +{ + va_list args; + va_start(args, fmt); + const int r = oakcore_stringutils_format_v(buf, buf_size, fmt, args); + va_end(args); + return r; +} + +static void test_format() +{ + char buf[64]; + + // Format with mixed argument types + const int needed = oakcore_stringutils_format(buf, sizeof(buf), "%d-%s-%.2f", + 42, "mid", 1.5); + assert(needed == 11); + assert(strcmp(buf, "42-mid-1.50") == 0); + + // NULL buffer queries the required size + assert(oakcore_stringutils_format(NULL, 0, "%d-%s-%.2f", 42, "mid", + 1.5) == needed); + + // Too-small buffer truncates but still reports the full required size + char small[5]; + const int needed2 = + oakcore_stringutils_format(small, sizeof(small), "%s", "abcdefgh"); + assert(needed2 == 8); + assert(strcmp(small, "abcd") == 0); + + // va_list form produces identical results + char buf2[64]; + const int needed3 = format_forward(buf2, sizeof(buf2), "%d-%s-%.2f", 42, + "mid", 1.5); + assert(needed3 == needed); + assert(strcmp(buf2, buf) == 0); +} + +int main() +{ + test_split(); + test_split_regex(); + test_to_int(); + test_format(); + + printf("oakcore_stringutils_test: all tests passed\n"); + return 0; +} diff --git a/core/tests/oakcore_timecodefunctions_test.cpp b/core/tests/oakcore_timecodefunctions_test.cpp new file mode 100644 index 000000000..6ba12aa01 --- /dev/null +++ b/core/tests/oakcore_timecodefunctions_test.cpp @@ -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 . + +***/ + +#include +#include +#include +#include + +#include "olive/core/oakcore/timecodefunctions.h" + +int main() +{ + char buf[64]; + int ok = 0; + OakRational *r = NULL; + + /* Common timebases: 30 fps non-drop, 29.97 drop-frame, 60 fps */ + OakRational *tb_30 = oakcore_rational_create_nd(1, 30); + OakRational *tb_df = oakcore_rational_create_nd(1001, 30000); + OakRational *tb_60 = oakcore_rational_create_nd(1, 60); + + /* oakcore_timecode_time_to_timecode: drop-frame ( Olive's own test case ) */ + { + OakRational *time = oakcore_rational_create(1); + const int size = oakcore_timecode_time_to_timecode( + time, tb_df, OAK_TIMECODE_DISPLAY_DROP_FRAME, 0, NULL, 0); + assert(size == 11); + assert(oakcore_timecode_time_to_timecode( + time, tb_df, OAK_TIMECODE_DISPLAY_DROP_FRAME, 0, buf, + sizeof(buf)) == 11); + assert(strcmp(buf, "00:00:01;00") == 0); + /* Truncating buffer still reports the full required size */ + assert(oakcore_timecode_time_to_timecode( + time, tb_df, OAK_TIMECODE_DISPLAY_DROP_FRAME, 0, buf, + 6) == 11); + assert(strcmp(buf, "00:00") == 0); + oakcore_rational_free(time); + } + + /* oakcore_timecode_time_to_timecode: invalid timebase */ + { + OakRational *time = oakcore_rational_create(0); + OakRational *bizarre = oakcore_rational_create(156632219); + assert(oakcore_timecode_time_to_timecode( + time, bizarre, OAK_TIMECODE_DISPLAY_DROP_FRAME, 0, buf, + sizeof(buf)) == 16); + assert(strcmp(buf, "INVALID TIMEBASE") == 0); + oakcore_rational_free(bizarre); + oakcore_rational_free(time); + } + + /* oakcore_timecode_time_to_timecode: non-drop, plus sign, negative */ + { + OakRational *time = oakcore_rational_create_nd(3, 2); + assert(oakcore_timecode_time_to_timecode( + time, tb_30, OAK_TIMECODE_DISPLAY_NON_DROP_FRAME, 0, buf, + sizeof(buf)) == 11); + assert(strcmp(buf, "00:00:01:15") == 0); + assert(oakcore_timecode_time_to_timecode( + time, tb_30, OAK_TIMECODE_DISPLAY_NON_DROP_FRAME, 1, buf, + sizeof(buf)) == 12); + assert(strcmp(buf, "+00:00:01:15") == 0); + oakcore_rational_free(time); + time = oakcore_rational_create_nd(-3, 2); + assert(oakcore_timecode_time_to_timecode( + time, tb_30, OAK_TIMECODE_DISPLAY_NON_DROP_FRAME, 0, buf, + sizeof(buf)) == 12); + assert(strcmp(buf, "-00:00:01:15") == 0); + oakcore_rational_free(time); + } + + /* oakcore_timecode_time_to_timecode: seconds / frames / milliseconds */ + { + OakRational *time = oakcore_rational_create_nd(123, 2); /* 61.5 s */ + assert(oakcore_timecode_time_to_timecode( + time, tb_30, OAK_TIMECODE_DISPLAY_SECONDS, 0, buf, + sizeof(buf)) == 12); + assert(strcmp(buf, "00:01:01.500") == 0); + oakcore_rational_free(time); + time = oakcore_rational_create_nd(3, 2); + assert(oakcore_timecode_time_to_timecode( + time, tb_30, OAK_TIMECODE_DISPLAY_FRAMES, 0, buf, + sizeof(buf)) == 2); + assert(strcmp(buf, "45") == 0); + assert(oakcore_timecode_time_to_timecode( + time, tb_30, OAK_TIMECODE_DISPLAY_MILLISECONDS, 0, buf, + sizeof(buf)) == 4); + assert(strcmp(buf, "1500") == 0); + oakcore_rational_free(time); + } + + /* oakcore_timecode_timecode_to_time: non-drop */ + r = oakcore_timecode_timecode_to_time("00:00:01:15", tb_30, + OAK_TIMECODE_DISPLAY_NON_DROP_FRAME, + &ok); + assert(ok == 1); + assert(oakcore_rational_numerator(r) == 3 && + oakcore_rational_denominator(r) == 2); + oakcore_rational_free(r); + + /* oakcore_timecode_timecode_to_time: drop-frame */ + r = oakcore_timecode_timecode_to_time("00:00:01;00", tb_df, + OAK_TIMECODE_DISPLAY_DROP_FRAME, + &ok); + assert(ok == 1); + assert(oakcore_rational_numerator(r) == 1001 && + oakcore_rational_denominator(r) == 1000); + oakcore_rational_free(r); + + /* oakcore_timecode_timecode_to_time: seconds / frames / milliseconds */ + r = oakcore_timecode_timecode_to_time("00:00:01.5", tb_30, + OAK_TIMECODE_DISPLAY_SECONDS, &ok); + assert(ok == 1); + assert(oakcore_rational_numerator(r) == 3 && + oakcore_rational_denominator(r) == 2); + oakcore_rational_free(r); + r = oakcore_timecode_timecode_to_time("45", tb_30, + OAK_TIMECODE_DISPLAY_FRAMES, &ok); + assert(ok == 1); + assert(oakcore_rational_numerator(r) == 3 && + oakcore_rational_denominator(r) == 2); + oakcore_rational_free(r); + r = oakcore_timecode_timecode_to_time("1500", tb_30, + OAK_TIMECODE_DISPLAY_MILLISECONDS, + &ok); + assert(ok == 1); + assert(oakcore_rational_numerator(r) == 3 && + oakcore_rational_denominator(r) == 2); + oakcore_rational_free(r); + + /* oakcore_timecode_timecode_to_time: invalid and NULL input */ + r = oakcore_timecode_timecode_to_time("abc", tb_30, + OAK_TIMECODE_DISPLAY_NON_DROP_FRAME, + &ok); + assert(ok == 0); + assert(oakcore_rational_numerator(r) == 0); + oakcore_rational_free(r); + r = oakcore_timecode_timecode_to_time(NULL, tb_30, + OAK_TIMECODE_DISPLAY_NON_DROP_FRAME, + &ok); + assert(ok == 0); + oakcore_rational_free(r); + /* ok pointer itself may be NULL */ + r = oakcore_timecode_timecode_to_time("45", tb_30, + OAK_TIMECODE_DISPLAY_FRAMES, NULL); + assert(oakcore_rational_to_double(r) == 1.5); + oakcore_rational_free(r); + + /* oakcore_timecode_time_to_string */ + assert(oakcore_timecode_time_to_string(3661000, NULL, 0) == 8); + assert(oakcore_timecode_time_to_string(3661000, buf, sizeof(buf)) == 8); + assert(strcmp(buf, "01:01:01") == 0); + assert(oakcore_timecode_time_to_string(0, buf, sizeof(buf)) == 8); + assert(strcmp(buf, "00:00:00") == 0); + + /* oakcore_timecode_snap_time_to_timebase */ + { + OakRational *time = oakcore_rational_create_nd(102, 100); /* 1.02 s */ + r = oakcore_timecode_snap_time_to_timebase( + time, tb_30, OAK_TIMECODE_ROUNDING_ROUND); + assert(oakcore_rational_numerator(r) == 31 && + oakcore_rational_denominator(r) == 30); + oakcore_rational_free(r); + oakcore_rational_free(time); + time = oakcore_rational_create_nd(151, 100); /* 1.51 s -> 45.3 frames */ + r = oakcore_timecode_snap_time_to_timebase( + time, tb_30, OAK_TIMECODE_ROUNDING_FLOOR); + assert(oakcore_rational_numerator(r) == 3 && + oakcore_rational_denominator(r) == 2); + oakcore_rational_free(r); + r = oakcore_timecode_snap_time_to_timebase( + time, tb_30, OAK_TIMECODE_ROUNDING_CEIL); + assert(oakcore_rational_numerator(r) == 23 && + oakcore_rational_denominator(r) == 15); + oakcore_rational_free(r); + oakcore_rational_free(time); + } + + /* oakcore_timecode_time_to_timestamp (Rational) */ + { + OakRational *time = oakcore_rational_create_nd(3, 2); + assert(oakcore_timecode_time_to_timestamp( + time, tb_30, OAK_TIMECODE_ROUNDING_ROUND) == 45); + oakcore_rational_free(time); + time = oakcore_rational_create_nd(151, 100); /* 45.3 frames */ + assert(oakcore_timecode_time_to_timestamp( + time, tb_30, OAK_TIMECODE_ROUNDING_ROUND) == 45); + assert(oakcore_timecode_time_to_timestamp( + time, tb_30, OAK_TIMECODE_ROUNDING_FLOOR) == 45); + assert(oakcore_timecode_time_to_timestamp( + time, tb_30, OAK_TIMECODE_ROUNDING_CEIL) == 46); + oakcore_rational_free(time); + } + + /* oakcore_timecode_time_to_timestamp_d (double) */ + assert(oakcore_timecode_time_to_timestamp_d(1.5, tb_30, + OAK_TIMECODE_ROUNDING_ROUND) == + 45); + assert(oakcore_timecode_time_to_timestamp_d(NAN, tb_30, + OAK_TIMECODE_ROUNDING_ROUND) == + 0); + + /* oakcore_timecode_rescale_timestamp */ + assert(oakcore_timecode_rescale_timestamp(30, tb_30, tb_60) == 60); + assert(oakcore_timecode_rescale_timestamp(30, tb_30, tb_30) == 30); + { + /* 1 * (1*4) / (5*3) = 0.2666... -> nearest is 0 */ + OakRational *src = oakcore_rational_create_nd(1, 5); + OakRational *dst = oakcore_rational_create_nd(3, 4); + assert(oakcore_timecode_rescale_timestamp(1, src, dst) == 0); + /* ... but ceil rounds up to 1 */ + assert(oakcore_timecode_rescale_timestamp_ceil(1, src, dst) == 1); + oakcore_rational_free(dst); + oakcore_rational_free(src); + } + assert(oakcore_timecode_rescale_timestamp_ceil(30, tb_30, tb_60) == 60); + + /* oakcore_timecode_timestamp_to_time */ + r = oakcore_timecode_timestamp_to_time(45, tb_30); + assert(oakcore_rational_numerator(r) == 3 && + oakcore_rational_denominator(r) == 2); + oakcore_rational_free(r); + r = oakcore_timecode_timestamp_to_time(0, tb_30); + assert(oakcore_rational_to_double(r) == 0.0); + oakcore_rational_free(r); + + /* oakcore_timecode_timebase_is_drop_frame */ + assert(oakcore_timecode_timebase_is_drop_frame(tb_df) == 1); + assert(oakcore_timecode_timebase_is_drop_frame(tb_30) == 0); + + oakcore_rational_free(tb_60); + oakcore_rational_free(tb_df); + oakcore_rational_free(tb_30); + + printf("oakcore_timecodefunctions_test: all tests passed\n"); + return 0; +} diff --git a/core/tests/oakcore_timerange_test.cpp b/core/tests/oakcore_timerange_test.cpp new file mode 100644 index 000000000..5d7b40637 --- /dev/null +++ b/core/tests/oakcore_timerange_test.cpp @@ -0,0 +1,352 @@ +/*** + + 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 . + +***/ + +/** + * @file oakcore_timerange_test.cpp + * @brief Smoke test for the OakTimeRange C ABI + * + * Pure C API test: no gtest, no C++ wrapper classes. Every allocated handle + * is released with the matching free function. + */ + +#include +#include +#include + +#include "olive/core/oakcore/timerange.h" + +static OakRational *mk_time(int n) +{ + return oakcore_rational_create(n); +} + +static OakTimeRange *mk_range(int in, int out) +{ + OakRational *ri = mk_time(in); + OakRational *ro = mk_time(out); + OakTimeRange *r = oakcore_timerange_create_io(ri, ro); + oakcore_rational_free(ri); + oakcore_rational_free(ro); + return r; +} + +static void expect_int_time(const OakRational *r, int expected) +{ + assert(oakcore_rational_numerator(r) == expected); + assert(oakcore_rational_denominator(r) == 1); +} + +static void expect_range(const OakTimeRange *r, int in, int out) +{ + OakRational *ri = oakcore_timerange_in(r); + OakRational *ro = oakcore_timerange_out(r); + expect_int_time(ri, in); + expect_int_time(ro, out); + oakcore_rational_free(ri); + oakcore_rational_free(ro); +} + +static void test_create_default(void) +{ + OakTimeRange *r = oakcore_timerange_create(); + expect_range(r, 0, 0); + + OakRational *length = oakcore_timerange_length(r); + expect_int_time(length, 0); + oakcore_rational_free(length); + + oakcore_timerange_free(r); +} + +static void test_create_and_getters(void) +{ + OakTimeRange *r = mk_range(10, 20); + expect_range(r, 10, 20); + + OakRational *length = oakcore_timerange_length(r); + expect_int_time(length, 10); + oakcore_rational_free(length); + + oakcore_timerange_free(r); +} + +static void test_normalize_swaps_in_out(void) +{ + // out earlier than in must be swapped + OakTimeRange *r = mk_range(20, 10); + expect_range(r, 10, 20); + + OakRational *length = oakcore_timerange_length(r); + expect_int_time(length, 10); + oakcore_rational_free(length); + + oakcore_timerange_free(r); +} + +static void test_length_nan_on_extremes(void) +{ + // RATIONAL_MIN/RATIONAL_MAX endpoints make the length NaN + OakTimeRange *r = mk_range(INT_MIN, 0); + OakRational *length = oakcore_timerange_length(r); + assert(oakcore_rational_is_nan(length) == 1); + oakcore_rational_free(length); + oakcore_timerange_free(r); + + r = mk_range(0, INT_MAX); + length = oakcore_timerange_length(r); + assert(oakcore_rational_is_nan(length) == 1); + oakcore_rational_free(length); + oakcore_timerange_free(r); +} + +static void test_copy_and_equal(void) +{ + OakTimeRange *r = mk_range(3, 7); + OakTimeRange *copy = oakcore_timerange_copy(r); + + assert(oakcore_timerange_equal(r, copy) == 1); + expect_range(copy, 3, 7); + + // Mutating the copy must not affect the original (deep ownership) + OakRational *nine = mk_time(9); + oakcore_timerange_set_in(copy, nine); + oakcore_rational_free(nine); + + // set_in(9) with out==7 normalizes by swapping to (7, 9) + expect_range(copy, 7, 9); + assert(oakcore_timerange_equal(r, copy) == 0); + expect_range(r, 3, 7); + + oakcore_timerange_free(copy); + oakcore_timerange_free(r); +} + +static void test_setters(void) +{ + OakTimeRange *r = mk_range(0, 10); + OakRational *t = mk_time(5); + + oakcore_timerange_set_in(r, t); + expect_range(r, 5, 10); + + oakcore_timerange_set_out(r, t); + // set_out(5) with in==5 collapses to a zero-length range + expect_range(r, 5, 5); + + { + OakRational *in = mk_time(30); + OakRational *out = mk_time(20); + oakcore_timerange_set_range(r, in, out); + oakcore_rational_free(in); + oakcore_rational_free(out); + } + // set_range also normalizes + expect_range(r, 20, 30); + + oakcore_rational_free(t); + oakcore_timerange_free(r); +} + +static void test_overlaps_with(void) +{ + OakTimeRange *a = mk_range(0, 10); + OakTimeRange *b = mk_range(10, 20); + OakTimeRange *c = mk_range(5, 15); + + // Touching ranges (0,10) vs (10,20): in_inclusive governs the + // other.out-vs-self.in comparison, out_inclusive the other.in-vs-self.out + // one, so the results are asymmetric for mixed flags + assert(oakcore_timerange_overlaps_with(a, b, 1, 1) == 1); + assert(oakcore_timerange_overlaps_with(a, b, 0, 0) == 0); + assert(oakcore_timerange_overlaps_with(a, b, 1, 0) == 0); + assert(oakcore_timerange_overlaps_with(a, b, 0, 1) == 1); + + // Genuinely overlapping ranges overlap under any inclusivity + assert(oakcore_timerange_overlaps_with(a, c, 1, 1) == 1); + assert(oakcore_timerange_overlaps_with(a, c, 0, 0) == 1); + + oakcore_timerange_free(c); + oakcore_timerange_free(b); + oakcore_timerange_free(a); +} + +static void test_contains_range(void) +{ + OakTimeRange *outer = mk_range(0, 30); + OakTimeRange *inner = mk_range(5, 10); + OakTimeRange *same = mk_range(0, 30); + OakTimeRange *partial = mk_range(5, 40); + + assert(oakcore_timerange_contains_range(outer, inner, 1, 1) == 1); + assert(oakcore_timerange_contains_range(outer, inner, 0, 0) == 1); + + // Identical ranges: contained inclusively, not exclusively + assert(oakcore_timerange_contains_range(outer, same, 1, 1) == 1); + assert(oakcore_timerange_contains_range(outer, same, 0, 0) == 0); + + assert(oakcore_timerange_contains_range(outer, partial, 1, 1) == 0); + assert(oakcore_timerange_contains_range(inner, outer, 1, 1) == 0); + + oakcore_timerange_free(partial); + oakcore_timerange_free(same); + oakcore_timerange_free(inner); + oakcore_timerange_free(outer); +} + +static void test_contains_time(void) +{ + OakTimeRange *r = mk_range(0, 10); + OakRational *inside = mk_time(5); + OakRational *edge_in = mk_time(0); + OakRational *edge_out = mk_time(10); + OakRational *outside = mk_time(-1); + + // contains(time) is in-inclusive but out-exclusive + assert(oakcore_timerange_contains_time(r, inside) == 1); + assert(oakcore_timerange_contains_time(r, edge_in) == 1); + assert(oakcore_timerange_contains_time(r, edge_out) == 0); + assert(oakcore_timerange_contains_time(r, outside) == 0); + + oakcore_rational_free(outside); + oakcore_rational_free(edge_out); + oakcore_rational_free(edge_in); + oakcore_rational_free(inside); + oakcore_timerange_free(r); +} + +static void test_combine_and_intersect(void) +{ + OakTimeRange *a = mk_range(0, 10); + OakTimeRange *b = mk_range(20, 30); + + OakTimeRange *combined = oakcore_timerange_combined(a, b); + expect_range(combined, 0, 30); + oakcore_timerange_free(combined); + + combined = oakcore_timerange_combine(b, a); + expect_range(combined, 0, 30); + oakcore_timerange_free(combined); + + // Disjoint ranges intersect to (20, 10), which normalize() swaps to (10, 20) + OakTimeRange *intersected = oakcore_timerange_intersected(a, b); + expect_range(intersected, 10, 20); + oakcore_timerange_free(intersected); + + oakcore_timerange_free(b); + oakcore_timerange_free(a); + + a = mk_range(0, 20); + b = mk_range(10, 30); + + intersected = oakcore_timerange_intersected(a, b); + expect_range(intersected, 10, 20); + oakcore_timerange_free(intersected); + + intersected = oakcore_timerange_intersect(b, a); + expect_range(intersected, 10, 20); + oakcore_timerange_free(intersected); + + oakcore_timerange_free(b); + oakcore_timerange_free(a); +} + +static void test_arithmetic(void) +{ + OakTimeRange *r = mk_range(0, 10); + OakRational *five = mk_time(5); + + OakTimeRange *shifted = oakcore_timerange_add(r, five); + expect_range(shifted, 5, 15); + + OakTimeRange *back = oakcore_timerange_sub(shifted, five); + expect_range(back, 0, 10); + oakcore_timerange_free(back); + oakcore_timerange_free(shifted); + + oakcore_timerange_add_assign(r, five); + expect_range(r, 5, 15); + + oakcore_timerange_sub_assign(r, five); + expect_range(r, 0, 10); + + OakRational *length = oakcore_timerange_length(r); + expect_int_time(length, 10); + oakcore_rational_free(length); + + oakcore_rational_free(five); + oakcore_timerange_free(r); +} + +static void test_split(void) +{ + OakTimeRange *r = mk_range(0, 10); + + // Size query, both directly and through the NULL-buffer form + assert(oakcore_timerange_split_count(r, 4) == 3); + assert(oakcore_timerange_split(r, 4, NULL, 0) == 3); + + // Full split: (0,4), (4,8), (8,10) + OakTimeRange *chunks[3]; + assert(oakcore_timerange_split(r, 4, chunks, 3) == 3); + expect_range(chunks[0], 0, 4); + expect_range(chunks[1], 4, 8); + expect_range(chunks[2], 8, 10); + for (int i = 0; i < 3; i++) { + oakcore_timerange_free(chunks[i]); + } + + // A too-small buffer receives a prefix, the return value is the total + OakTimeRange *prefix[2]; + assert(oakcore_timerange_split(r, 4, prefix, 2) == 3); + expect_range(prefix[0], 0, 4); + expect_range(prefix[1], 4, 8); + oakcore_timerange_free(prefix[0]); + oakcore_timerange_free(prefix[1]); + + // Zero-length range splits into exactly one chunk + OakTimeRange *zero = mk_range(5, 5); + assert(oakcore_timerange_split_count(zero, 4) == 1); + OakTimeRange *single = NULL; + assert(oakcore_timerange_split(zero, 4, &single, 1) == 1); + expect_range(single, 5, 5); + oakcore_timerange_free(single); + oakcore_timerange_free(zero); + + oakcore_timerange_free(r); +} + +int main(void) +{ + test_create_default(); + test_create_and_getters(); + test_normalize_swaps_in_out(); + test_length_nan_on_extremes(); + test_copy_and_equal(); + test_setters(); + test_overlaps_with(); + test_contains_range(); + test_contains_time(); + test_combine_and_intersect(); + test_arithmetic(); + test_split(); + + printf("oakcore_timerange_test: all tests passed\n"); + return 0; +} diff --git a/core/tests/rational-test.cpp b/core/tests/rational-test.cpp deleted file mode 100644 index 2271b853d..000000000 --- a/core/tests/rational-test.cpp +++ /dev/null @@ -1,109 +0,0 @@ -/*** - - Olive - Non-Linear Video Editor - Copyright (C) 2023 Olive Studios LLC - Modifications Copyright (C) 2025 mikesolar - - This program is free software: you can redistribute it and/or modify - it under the terms of the GNU General Public License as published by - the Free Software Foundation, either version 3 of the License, or - (at your option) any later version. - - This program is distributed in the hope that it will be useful, - but WITHOUT ANY WARRANTY; without even the implied warranty of - MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - GNU General Public License for more details. - - You should have received a copy of the GNU General Public License - along with this program. If not, see . - -***/ - -#include - -#include "util/rational.h" -#include "util/tests.h" - -using namespace olive::core; - -bool rational_to_from_string_test() -{ - Rational r(1, 30); - - std::string s = r.toString(); - - Rational r2 = Rational::fromString(s); - - return r == r2; -} - -bool rational_to_from_string_test2() -{ - Rational r(69, 420); - - std::string s = r.toString(); - - Rational r2 = Rational::fromString(s); - - return r == r2; -} - -bool rational_defaults() -{ - // By default, rationals are valid 0/1 - Rational basic_constructor; - - if (!basic_constructor.isNull()) { - return false; - } - - if (basic_constructor.isNaN()) { - return false; - } - - return true; -} - -bool rational_nan() -{ - // Create a NaN with a 0 denominator - Rational nan = Rational(0, 0); - if (!nan.isNaN()) - return false; - if (!nan.isNull()) - return false; - - // Create a non-NaN with a zero numerator - Rational zero_nonnan(0, 999); - if (!zero_nonnan.isNull()) - return false; - if (zero_nonnan.isNaN()) - return false; - - // Create a non-NaN with a non-zero numerator - Rational nonzer_nonnan(1, 30); - if (nonzer_nonnan.isNull()) - return false; - if (nonzer_nonnan.isNaN()) - return false; - - return true; -} - -bool rational_nan_constant() -{ - return Rational::NaN.isNaN(); -} - -int main() -{ - Tester t; - - t.add("Rational::defaults", rational_defaults); - t.add("Rational::NaN", rational_nan); - t.add("Rational::NaN_constant", rational_nan_constant); - t.add("Rational::toString/fromString", rational_to_from_string_test); - t.add("Rational::toString/fromString2", rational_to_from_string_test2); - - return t.exec(); -} diff --git a/core/tests/stringutils-test.cpp b/core/tests/stringutils-test.cpp deleted file mode 100644 index 684b83cf6..000000000 --- a/core/tests/stringutils-test.cpp +++ /dev/null @@ -1,47 +0,0 @@ -/*** - - Olive - Non-Linear Video Editor - Copyright (C) 2023 Olive Studios LLC - Modifications Copyright (C) 2025 mikesolar - - This program is free software: you can redistribute it and/or modify - it under the terms of the GNU General Public License as published by - the Free Software Foundation, either version 3 of the License, or - (at your option) any later version. - - This program is distributed in the hope that it will be useful, - but WITHOUT ANY WARRANTY; without even the implied warranty of - MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - GNU General Public License for more details. - - You should have received a copy of the GNU General Public License - along with this program. If not, see . - -***/ - -#include - -#include "util/stringutils.h" -#include "util/tests.h" - -using namespace olive::core; - -bool stringutils_format_test() -{ - const char *expected = "Hello, world!"; - std::string f = StringUtils::format("%s, %s!", "Hello", "world"); - if (strcmp(f.c_str(), expected) != 0) { - return false; - } - - return true; -} - -int main() -{ - Tester t; - - t.add("StringUtils::format", stringutils_format_test); - - return t.exec(); -} diff --git a/core/tests/timecode-test.cpp b/core/tests/timecode-test.cpp deleted file mode 100644 index bf446bf08..000000000 --- a/core/tests/timecode-test.cpp +++ /dev/null @@ -1,65 +0,0 @@ -/*** - - Olive - Non-Linear Video Editor - Copyright (C) 2023 Olive Studios LLC - Modifications Copyright (C) 2025 mikesolar - - This program is free software: you can redistribute it and/or modify - it under the terms of the GNU General Public License as published by - the Free Software Foundation, either version 3 of the License, or - (at your option) any later version. - - This program is distributed in the hope that it will be useful, - but WITHOUT ANY WARRANTY; without even the implied warranty of - MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - GNU General Public License for more details. - - You should have received a copy of the GNU General Public License - along with this program. If not, see . - -***/ - -#include - -#include "util/timecodefunctions.h" -#include "util/tests.h" - -using namespace olive::core; - -bool timecodefunctions_time_to_timecode_test() -{ - Rational drop_frame_30(1001, 30000); - - std::string timecode = Timecode::time_to_timecode( - Rational(1), drop_frame_30, Timecode::kTimecodeDropFrame); - if (strcmp(timecode.c_str(), "00:00:01;00") != 0) { - return false; - } - - return true; -} - -bool timecodefunctions_time_to_timecode_test2() -{ - Rational bizarre_timebase(156632219); - - std::string timecode = Timecode::time_to_timecode( - Rational(0), bizarre_timebase, Timecode::kTimecodeDropFrame); - if (strcmp(timecode.c_str(), "INVALID TIMEBASE") != 0) { - return false; - } - - return true; -} - -int main() -{ - Tester t; - - t.add("Timecode::time_to_timecode", - timecodefunctions_time_to_timecode_test); - t.add("Timecode::time_to_timecode2", - timecodefunctions_time_to_timecode_test2); - - return t.exec(); -} diff --git a/core/tests/timerange-test.cpp b/core/tests/timerange-test.cpp deleted file mode 100644 index b7f719049..000000000 --- a/core/tests/timerange-test.cpp +++ /dev/null @@ -1,124 +0,0 @@ -/*** - - Olive - Non-Linear Video Editor - Copyright (C) 2023 Olive Studios LLC - Modifications Copyright (C) 2025 mikesolar - - This program is free software: you can redistribute it and/or modify - it under the terms of the GNU General Public License as published by - the Free Software Foundation, either version 3 of the License, or - (at your option) any later version. - - This program is distributed in the hope that it will be useful, - but WITHOUT ANY WARRANTY; without even the implied warranty of - MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - GNU General Public License for more details. - - You should have received a copy of the GNU General Public License - along with this program. If not, see . - -***/ - -#include - -#include "util/timerange.h" -#include "util/tests.h" - -using namespace olive::core; - -bool timerangelist_remove_test() -{ - TimeRangeList t; - - t.insert(TimeRange(0, 30)); - t.remove(TimeRange(2, 5)); - - return true; -} - -bool timerangelist_mergeadjacent_test() -{ - TimeRangeList t; - - // TimeRangeList should merge 1 and 3 together since they're adjacent - t.insert(TimeRange(0, 6)); - t.insert(TimeRange(20, 30)); - t.insert(TimeRange(6, 10)); - - if (!(t.size() == 2)) { - return false; - } - if (!(t.first() == TimeRange(20, 30))) { - return false; - } - if (!(t.at(1) == TimeRange(0, 10))) { - return false; - } - - // TimeRangeList should ignore these because it's already contained - TimeRangeList noop_test = t; - - noop_test.insert(TimeRange(4, 7)); - if (!(noop_test == t)) { - return false; - } - - noop_test.insert(TimeRange(0, 3)); - if (!(noop_test == t)) { - return false; - } - - noop_test.insert(TimeRange(25, 30)); - if (!(noop_test == t)) { - return false; - } - - // TimeRangeList should combine all these together - TimeRangeList combine_test_no_overlap = t; - combine_test_no_overlap.insert(TimeRange(10, 20)); - if (!(combine_test_no_overlap.size() == 1)) { - return false; - } - if (!(combine_test_no_overlap.first() == TimeRange(0, 30))) { - return false; - } - - TimeRangeList combine_test_in_overlap = t; - combine_test_in_overlap.insert(TimeRange(9, 20)); - if (!(combine_test_in_overlap.size() == 1)) { - return false; - } - if (!(combine_test_in_overlap.first() == TimeRange(0, 30))) { - return false; - } - - TimeRangeList combine_test_out_overlap = t; - combine_test_out_overlap.insert(TimeRange(10, 21)); - if (!(combine_test_out_overlap.size() == 1)) { - return false; - } - if (!(combine_test_out_overlap.first() == TimeRange(0, 30))) { - return false; - } - - TimeRangeList combine_test_both_overlap = t; - combine_test_both_overlap.insert(TimeRange(9, 21)); - if (!(combine_test_both_overlap.size() == 1)) { - return false; - } - if (!(combine_test_both_overlap.first() == TimeRange(0, 30))) { - return false; - } - - return true; -} - -int main() -{ - Tester t; - - t.add("TimeRangeList::remove", timerangelist_remove_test); - t.add("TimeRangeList::merge_adjacent", timerangelist_mergeadjacent_test); - - return t.exec(); -} diff --git a/tests/gtest/CMakeLists.txt b/tests/gtest/CMakeLists.txt index 80a9b7b72..ebe242523 100644 --- a/tests/gtest/CMakeLists.txt +++ b/tests/gtest/CMakeLists.txt @@ -198,8 +198,11 @@ else() endif() if (WIN32) - # Windows has no RPATH: ffmpeg_bridge.dll must sit next to the executable + # Windows has no RPATH: shared libraries must sit next to the executable add_custom_command(TARGET olive-gtest POST_BUILD COMMAND ${CMAKE_COMMAND} -E copy_if_different $ $) + add_custom_command(TARGET olive-gtest POST_BUILD + COMMAND ${CMAKE_COMMAND} -E copy_if_different + $ $) endif() diff --git a/worker/CMakeLists.txt b/worker/CMakeLists.txt index 5fe6721ae..37c7c5b31 100644 --- a/worker/CMakeLists.txt +++ b/worker/CMakeLists.txt @@ -70,6 +70,14 @@ endif () # copies, Linux install() into the same bin directory) add_dependencies(olive-editor olive-render-worker) +if (WIN32) + # Windows has no RPATH: shared libraries must sit next to the executable + add_custom_command(TARGET olive-render-worker POST_BUILD + COMMAND ${CMAKE_COMMAND} -E copy_if_different $ $ + COMMAND ${CMAKE_COMMAND} -E copy_if_different $ $ + ) +endif () + # Install the render worker alongside the editor on Linux. if (UNIX AND NOT APPLE) install(TARGETS olive-render-worker RUNTIME DESTINATION bin)