From 0f41620a0bbcb5c78ca6f11c326e8abab73f97e0 Mon Sep 17 00:00:00 2001 From: Mike Solar Date: Wed, 15 Jul 2026 21:55:11 +0800 Subject: [PATCH] core: remove direct FFmpeg dependency from libolivecore - rational: store num/den natively instead of AVRational; math operators re-implemented natively (ported av_reduce/av_d2q/av_cmp_q semantics, verified bit-exact against FFmpeg) - AudioParams: replace AVChannelLayout member with a plain uint64_t mask (new render/channellayout.h constants mirror AV_CH_LAYOUT_* values) - Timecode: native rescale (av_rescale_q/av_rescale_q_rnd equivalents) with 128-bit intermediate precision - core no longer finds or links FFMPEG::avutil Part of the FFmpeg isolation effort: all FFmpeg access is being moved behind a dedicated shared library (ffmpeg_bridge). --- core/CMakeLists.txt | 8 +- core/include/olive/core/render/audioparams.h | 136 ++---------- .../include/olive/core/render/channellayout.h | 63 ++++++ core/include/olive/core/util/fractionutils.h | 80 +++++++ core/include/olive/core/util/rational.h | 39 ++-- core/src/render/audioparams.cpp | 92 +------- core/src/render/samplebuffer.cpp | 5 +- core/src/util/fractionutils.cpp | 205 ++++++++++++++++++ core/src/util/rational.cpp | 118 ++++++---- core/src/util/timecodefunctions.cpp | 21 +- 10 files changed, 480 insertions(+), 287 deletions(-) create mode 100644 core/include/olive/core/render/channellayout.h create mode 100644 core/include/olive/core/util/fractionutils.h create mode 100644 core/src/util/fractionutils.cpp diff --git a/core/CMakeLists.txt b/core/CMakeLists.txt index 87b9a6d63..48e6e9201 100644 --- a/core/CMakeLists.txt +++ b/core/CMakeLists.txt @@ -27,11 +27,6 @@ set(CMAKE_CXX_EXTENSIONS OFF) list(APPEND CMAKE_MODULE_PATH "${CMAKE_CURRENT_SOURCE_DIR}/cmake") -# Link avutil -find_package(FFMPEG 6.0 REQUIRED - COMPONENTS avutil -) - # Link Imath find_package(Imath REQUIRED CONFIG) @@ -46,6 +41,7 @@ add_library(olivecore src/render/samplebuffer.cpp src/util/bezier.cpp src/util/color.cpp + src/util/fractionutils.cpp src/util/rational.cpp src/util/stringutils.cpp src/util/tests.cpp @@ -55,7 +51,6 @@ add_library(olivecore ) target_include_directories(olivecore PRIVATE - ${FFMPEG_INCLUDE_DIRS} "${CMAKE_CURRENT_SOURCE_DIR}/include/olive/core" "${CMAKE_SOURCE_DIR}/third_party/openfx/include/" ) @@ -63,7 +58,6 @@ target_include_directories(olivecore PRIVATE target_link_libraries(olivecore PRIVATE OpenGL::GL Imath::Imath - FFMPEG::avutil ) # Link OpenTimelineIO (optional) diff --git a/core/include/olive/core/render/audioparams.h b/core/include/olive/core/render/audioparams.h index 046b335df..961885744 100644 --- a/core/include/olive/core/render/audioparams.h +++ b/core/include/olive/core/render/audioparams.h @@ -23,13 +23,10 @@ #define LIBOLIVECORE_AUDIOPARAMS_H #include -extern "C" { -#include -} - #include #include +#include "channellayout.h" #include "sampleformat.h" #include "../util/rational.h" @@ -38,21 +35,10 @@ namespace olive::core /** * @brief Audio parameters class managing audio stream configuration - * - * CRITICAL NOTE: This class manages AVChannelLayout which contains dynamic memory - * (custom channel maps via u.map pointer). Prior to the Rule of Three implementation, - * shallow copies could occur when: - * - AudioParams stored in QVector (QVector reallocations) - * - AudioParams passed by value to RenderVideoParams - * - AudioParams copied during Node graph duplication in ProjectCopier - * - * When shallow copies occurred, one copy's set_channel_layout() could free the - * shared u.map pointer, corrupting other copies. This manifested as: - * - channel_layouts=0x0 errors in AudioProcessor::Open() - * - is_valid() returning false unexpectedly - * - * The Rule of Three (copy ctor, copy assignment, destructor) was added to ensure - * proper deep copies of AVChannelLayout using av_channel_layout_copy(). + * + * 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: @@ -62,59 +48,28 @@ public: */ AudioParams() : sample_rate_(0) - , channel_layout_{} + , channel_layout_mask_(0) , channel_count_(0) , format_(SampleFormat::INVALID) { set_default_footage_parameters(); } - /** - * @brief Constructor from AVChannelLayout (deep copy) - * @param sample_rate Audio sample rate (e.g., 48000) - * @param channel_layout FFmpeg channel layout (copied via av_channel_layout_copy) - * @param format Sample format (e.g., SampleFormat::F32P) - * - * NOTE: The channel_layout parameter is deep-copied. The original can be - * safely uninit'd after this constructor returns. - */ - AudioParams(const int &sample_rate, const AVChannelLayout &channel_layout, - const SampleFormat &format) - : sample_rate_(sample_rate) - , channel_layout_{} - , channel_count_(0) - , format_(format) - { - set_default_footage_parameters(); - timebase_ = sample_rate_as_time_base(); - av_channel_layout_uninit(&channel_layout_); - av_channel_layout_copy(&channel_layout_, &channel_layout); - - // Cache channel count from the copied layout - calculate_channel_count(); - } - /** * @brief Constructor from channel layout mask * @param sample_rate Audio sample rate - * @param channel_layout Channel layout mask (e.g., AV_CH_LAYOUT_STEREO) + * @param channel_layout Channel layout mask (e.g., kChannelLayoutStereo) * @param format Sample format - * - * This is the most common constructor used in Olive. The mask is converted - * to AVChannelLayout via av_channel_layout_from_mask(). */ AudioParams(const int &sample_rate, uint64_t channel_layout, const SampleFormat &format) : sample_rate_(sample_rate) - , channel_layout_{} + , channel_layout_mask_(channel_layout) , channel_count_(0) , format_(format) { set_default_footage_parameters(); timebase_ = sample_rate_as_time_base(); - av_channel_layout_uninit(&channel_layout_); - av_channel_layout_from_mask(&channel_layout_, channel_layout); - // Cache channel count calculate_channel_count(); } int sample_rate() const @@ -127,37 +82,21 @@ public: sample_rate_ = sample_rate; } - const AVChannelLayout &channel_layout() const - { - return channel_layout_; - } - /** - * @brief Set channel layout from AVChannelLayout (deep copy) - * @param channel_layout Source channel layout to copy - * - * CRITICAL: This function first uninitializes the current layout (freeing any - * dynamic memory), then deep-copies the new layout. This is safe only if - * copies are properly managed via Rule of Three. - * - * If called on a shallow-copied AudioParams, this would corrupt other copies - * that share the same u.map pointer. + * @brief Channel layout as a 64-bit mask (0 if unspecified) */ - void set_channel_layout(const AVChannelLayout &channel_layout) + const uint64_t &channel_layout() const { - av_channel_layout_uninit(&channel_layout_); - av_channel_layout_copy(&channel_layout_, &channel_layout); - calculate_channel_count(); + return channel_layout_mask_; } /** * @brief Set channel layout from mask - * @param mask Channel layout mask (e.g., AV_CH_LAYOUT_STEREO) + * @param mask Channel layout mask (e.g., kChannelLayoutStereo) */ void set_channel_layout(uint64_t mask) { - av_channel_layout_uninit(&channel_layout_); - av_channel_layout_from_mask(&channel_layout_, mask); + channel_layout_mask_ = mask; calculate_channel_count(); } rational time_base() const @@ -235,32 +174,6 @@ public: bool operator==(const AudioParams &other) const; bool operator!=(const AudioParams &other) const; - /** - * @name Rule of Three Implementation - * - * These are required because AVChannelLayout (FFmpeg >= 5.0) contains a union - * with a pointer member (u.map for custom channel maps). Without proper - * deep copy management: - * - * 1. Default copy constructor: Shallow copies u.map pointer, leading to - * double-free when original and copy are destroyed - * 2. Default copy assignment: Same issue as copy constructor - * 3. Default destructor: Doesn't free u.map, causing memory leaks - * - * The implementations use av_channel_layout_copy() and av_channel_layout_uninit() - * for proper FFmpeg-managed memory handling. - * - * Context where this matters: - * - QVector in FootageDescription (vector reallocations) - * - RenderVideoParams passing AudioParams by value - * - ProjectCopier duplicating node graphs with audio parameters - */ - ///@{ - AudioParams(const AudioParams &other); - AudioParams &operator=(const AudioParams &other); - ~AudioParams(); - ///@} - static const std::vector kSupportedChannelLayouts; static const std::vector kSupportedSampleRates; @@ -273,7 +186,7 @@ private: } /** - * @brief Updates channel_count_ from the current channel_layout_ + * @brief Updates channel_count_ from the current channel_layout_mask_ * Called after any channel layout modification. */ void calculate_channel_count(); @@ -281,23 +194,12 @@ private: int sample_rate_; ///< Audio sample rate in Hz (e.g., 48000) /** - * @brief FFmpeg channel layout structure - * - * WARNING: This struct contains a union with a pointer member (u.map) when - * using custom channel layouts (order == AV_CHANNEL_ORDER_CUSTOM). The pointer - * must be properly managed via av_channel_layout_copy/uninit. - * - * Layout variants: - * - order == AV_CHANNEL_ORDER_UNSPEC: u.mask is undefined, nb_channels valid - * - order == AV_CHANNEL_ORDER_NATIVE: u.mask contains channel bitmask - * - order == AV_CHANNEL_ORDER_CUSTOM: u.map points to AVChannelCustom array - * - * Corruption symptoms: - * - u.mask == 0 when order should be NATIVE - * - av_channel_layout_check() returns false - * - is_valid() returns false + * @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. */ - AVChannelLayout channel_layout_; + uint64_t channel_layout_mask_; int channel_count_; ///< Cached channel count from layout diff --git a/core/include/olive/core/render/channellayout.h b/core/include/olive/core/render/channellayout.h new file mode 100644 index 000000000..7e4c2ab40 --- /dev/null +++ b/core/include/olive/core/render/channellayout.h @@ -0,0 +1,63 @@ +/*** + + 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 LIBOLIVECORE_CHANNELLAYOUT_H +#define LIBOLIVECORE_CHANNELLAYOUT_H + +#include + +namespace olive::core +{ + +/** + * @brief Channel layout masks used throughout Olive + * + * Audio channel layouts are represented as plain 64-bit masks. The values + * deliberately mirror FFmpeg's AV_CH_LAYOUT_* constants so they can be passed + * straight through to the FFmpeg bridge library; the bridge unit tests + * static_assert each value against the real FFmpeg headers. + */ +inline constexpr uint64_t kChannelLayoutMono = 0x4; ///< AV_CH_LAYOUT_MONO +inline constexpr uint64_t kChannelLayoutStereo = 0x3; ///< AV_CH_LAYOUT_STEREO +inline constexpr uint64_t kChannelLayout2_1 = 0x103; ///< AV_CH_LAYOUT_2_1 +inline constexpr uint64_t kChannelLayout5Point1 = 0x60F; ///< AV_CH_LAYOUT_5POINT1 +inline constexpr uint64_t kChannelLayout7Point1 = 0x63F; ///< AV_CH_LAYOUT_7POINT1 + +/** + * @brief Number of channels in a layout mask (population count) + */ +inline int ChannelLayoutMaskChannelCount(uint64_t mask) +{ +#if defined(__GNUC__) || defined(__clang__) + return __builtin_popcountll(mask); +#else + int count = 0; + while (mask) { + mask &= mask - 1; + count++; + } + return count; +#endif +} + +} + +#endif // LIBOLIVECORE_CHANNELLAYOUT_H diff --git a/core/include/olive/core/util/fractionutils.h b/core/include/olive/core/util/fractionutils.h new file mode 100644 index 000000000..70fee45c8 --- /dev/null +++ b/core/include/olive/core/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 LIBOLIVECORE_FRACTIONUTILS_H +#define LIBOLIVECORE_FRACTIONUTILS_H + +#include + +namespace olive::core +{ + +/** + * @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. + */ + kNearInf, + + /** + * Round toward positive infinity. Equivalent to FFmpeg's AV_ROUND_UP. + */ + kUp +}; + +/** + * @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 ReduceFraction(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 CompareFractions(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 RescaleRnd(int64_t a, int64_t b, int64_t c, FractionRounding rnd); + +} + +#endif // LIBOLIVECORE_FRACTIONUTILS_H diff --git a/core/include/olive/core/util/rational.h b/core/include/olive/core/util/rational.h index a9b8573c7..523209c6f 100644 --- a/core/include/olive/core/util/rational.h +++ b/core/include/olive/core/util/rational.h @@ -22,10 +22,7 @@ #ifndef LIBOLIVECORE_RATIONAL_H #define LIBOLIVECORE_RATIONAL_H -extern "C" { -#include -} - +#include #include #ifdef USE_OTIO @@ -39,14 +36,14 @@ class rational { public: rational(const int &numerator = 0) { - r_.num = numerator; - r_.den = 1; + num_ = numerator; + den_ = 1; } rational(const int &numerator, const int &denominator) { - r_.num = numerator; - r_.den = denominator; + num_ = numerator; + den_ = denominator; fix_signs(); reduce(); @@ -54,13 +51,6 @@ public: rational(const rational &rhs) = default; - rational(const AVRational &r) - { - r_ = r; - - fix_signs(); - } - static rational fromDouble(const double &flt, bool *ok = nullptr); static rational fromString(const std::string &str, bool *ok = nullptr); @@ -94,18 +84,16 @@ public: } rational operator-() const { - return rational(r_.num, -r_.den); + return rational(num_, -den_); } bool operator!() const { - return !r_.num; + return !num_; } //Function: convert to double double toDouble() const; - AVRational toAVRational() const; - #ifdef USE_OTIO static rational fromRationalTime(const opentime::RationalTime &t) { @@ -126,29 +114,29 @@ public: // A NaN is always a null, but a null is not always a NaN bool isNull() const { - return r_.num == 0; + return num_ == 0; } // Returns whether this rational is not a valid number (denominator == 0) bool isNaN() const { - return r_.den == 0; + return den_ == 0; } const int &numerator() const { - return r_.num; + return num_; } const int &denominator() const { - return r_.den; + return den_; } std::string toString() const; friend std::ostream &operator<<(std::ostream &out, const rational &value) { - out << value.r_.num << '/' << value.r_.den; + out << value.num_ << '/' << value.den_; return out; } @@ -157,7 +145,8 @@ private: void fix_signs(); void reduce(); - AVRational r_; + int num_; + int den_; }; #define RATIONAL_MIN rational(INT_MIN) diff --git a/core/src/render/audioparams.cpp b/core/src/render/audioparams.cpp index 6ab50cf76..b0de3826a 100644 --- a/core/src/render/audioparams.cpp +++ b/core/src/render/audioparams.cpp @@ -40,16 +40,15 @@ const std::vector AudioParams::kSupportedSampleRates = { }; const std::vector AudioParams::kSupportedChannelLayouts = { - AV_CH_LAYOUT_MONO, AV_CH_LAYOUT_STEREO, AV_CH_LAYOUT_2_1, - AV_CH_LAYOUT_5POINT1, AV_CH_LAYOUT_7POINT1 + kChannelLayoutMono, kChannelLayoutStereo, kChannelLayout2_1, + kChannelLayout5Point1, kChannelLayout7Point1 }; bool AudioParams::operator==(const AudioParams &other) const { return format() == other.format() && sample_rate() == other.sample_rate() && time_base() == other.time_base() && - av_channel_layout_compare(&channel_layout_, - &other.channel_layout()) == 0; + channel_layout_mask_ == other.channel_layout_mask_; } bool AudioParams::operator!=(const AudioParams &other) const @@ -119,15 +118,11 @@ int64_t AudioParams::bytes_to_samples(const int64_t &bytes) const rational AudioParams::bytes_to_time(const int64_t &bytes) const { - assert(is_valid()); - return samples_to_time(bytes_to_samples(bytes)); } rational AudioParams::bytes_per_channel_to_time(const int64_t &bytes) const { - assert(is_valid()); - return samples_to_time(bytes_to_samples(bytes * channel_count())); } @@ -148,90 +143,13 @@ int AudioParams::bits_per_sample() const bool AudioParams::is_valid() const { - return (!time_base().isNull() && - av_channel_layout_check(&channel_layout_) && + return (!time_base().isNull() && channel_layout_mask_ != 0 && format_ > SampleFormat::INVALID && format_ < SampleFormat::COUNT); } void AudioParams::calculate_channel_count() { - channel_count_ = channel_layout().nb_channels; -} - -/** - * @brief Copy constructor - deep copies AVChannelLayout - * - * This is critical because AVChannelLayout::u.map is a pointer for custom - * channel layouts. Default copy would share the pointer, leading to double-free. - * - * The member initializer list initializes channel_layout_ to zero ({}), - * then av_channel_layout_copy performs the deep copy from other. - * - * @param other Source AudioParams to copy from - */ -AudioParams::AudioParams(const AudioParams &other) - : sample_rate_(other.sample_rate_) - , channel_layout_{} // Zero-initialize before FFmpeg copy - , channel_count_(other.channel_count_) - , format_(other.format_) - , enabled_(other.enabled_) - , stream_index_(other.stream_index_) - , duration_(other.duration_) - , timebase_(other.timebase_) -{ - // Deep copy AVChannelLayout using FFmpeg API - // This handles all layout types: unspecified, native (mask), and custom (map) - av_channel_layout_copy(&channel_layout_, &other.channel_layout_); -} - -/** - * @brief Copy assignment - cleans up existing layout before copying - * - * CRITICAL ORDER OF OPERATIONS: - * 1. Check for self-assignment (this != &other) - * 2. Copy all scalar members - * 3. Uninitialize current channel_layout_ (frees old u.map if present) - * 4. Deep copy from other's channel_layout_ - * - * Step 3 must happen before step 4 to avoid memory leaks. If we copied first, - * we'd lose the pointer to the old u.map that needs to be freed. - * - * @param other Source AudioParams to copy from - * @return Reference to this for chaining - */ -AudioParams &AudioParams::operator=(const AudioParams &other) -{ - if (this != &other) { - // Copy scalar members first (no dependencies) - sample_rate_ = other.sample_rate_; - format_ = other.format_; - channel_count_ = other.channel_count_; - enabled_ = other.enabled_; - stream_index_ = other.stream_index_; - duration_ = other.duration_; - timebase_ = other.timebase_; - - // Free current layout's dynamic memory (u.map if custom) - av_channel_layout_uninit(&channel_layout_); - - // Deep copy from other (includes allocating new u.map if needed) - av_channel_layout_copy(&channel_layout_, &other.channel_layout_); - } - return *this; -} - -/** - * @brief Destructor - frees AVChannelLayout dynamic memory - * - * av_channel_layout_uninit() handles all cases: - * - Unspecified/Native: No-op (no dynamic memory) - * - Custom: Frees u.map array - * - * Without this, custom channel layouts would leak memory. - */ -AudioParams::~AudioParams() -{ - av_channel_layout_uninit(&channel_layout_); + channel_count_ = ChannelLayoutMaskChannelCount(channel_layout_mask_); } } diff --git a/core/src/render/samplebuffer.cpp b/core/src/render/samplebuffer.cpp index 62c664369..2c237eba1 100644 --- a/core/src/render/samplebuffer.cpp +++ b/core/src/render/samplebuffer.cpp @@ -56,10 +56,7 @@ SampleBuffer::SampleBuffer(const AudioParams &audio_params, SampleBuffer SampleBuffer::rip_channel(int channel) const { AudioParams p = this->audio_params_; - AVChannelLayout layout; - av_channel_layout_from_mask(&layout, AV_CH_LAYOUT_MONO); - p.set_channel_layout(layout); - av_channel_layout_uninit(&layout); + p.set_channel_layout(kChannelLayoutMono); SampleBuffer b(p, this->sample_count_per_channel_); b.fast_set(*this, 0, channel); diff --git a/core/src/util/fractionutils.cpp b/core/src/util/fractionutils.cpp new file mode 100644 index 000000000..5f05163a8 --- /dev/null +++ b/core/src/util/fractionutils.cpp @@ -0,0 +1,205 @@ +/*** + + 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 "util/fractionutils.h" + +#include +#include +#include + +#include + +namespace olive::core +{ + +namespace +{ + +int64_t i64_gcd(int64_t a, int64_t b) +{ + if (a < 0) { + a = -a; + } + if (b < 0) { + b = -b; + } + + while (b) { + int64_t t = a % b; + a = b; + b = t; + } + + return a; +} + +} // namespace + +void ReduceFraction(int64_t &num, int64_t &den, int64_t max) +{ + if (den == 0) { + num = 0; + return; + } + + int sign = (num < 0) != (den < 0); + + int64_t gcd = i64_gcd(num, den); + if (gcd) { + num = (num < 0 ? -num : num) / gcd; + den = (den < 0 ? -den : den) / gcd; + } + + if (num <= max && den <= max) { + num = sign ? -num : num; + return; + } + + // Continued fraction approximation (ported from FFmpeg's av_reduce) + int64_t a0n = 0, a0d = 1; + int64_t a1n = 1, a1d = 0; + + while (den) { + int64_t x = num / den; + int64_t next_den = num - den * x; + int64_t a2n = x * a1n + a0n; + int64_t a2d = x * a1d + a0d; + + if (a2n > max || a2d > max) { + if (a1n) { + x = (max - a0n) / a1n; + } + if (a1d && (max - a0d) / a1d < x) { + x = (max - a0d) / a1d; + } + + if (den * (2 * x * a1d + a0d) > num * a1d) { + a1n = x * a1n + a0n; + a1d = x * a1d + a0d; + } + break; + } + + a0n = a1n; + a0d = a1d; + a1n = a2n; + a1d = a2d; + num = den; + den = next_den; + } + + num = sign ? -a1n : a1n; + den = a1d; +} + +int CompareFractions(int an, int ad, int bn, int bd) +{ + const int64_t tmp = an * int64_t(bd) - bn * int64_t(ad); + + if (tmp) { + return int(((tmp ^ ad ^ bd) >> 63) | 1); + } else if (bd && ad) { + return 0; + } else if (an && bn) { + return (an >> 31) - (bn >> 31); + } + + return INT_MIN; +} + +int64_t RescaleRnd(int64_t a, int64_t b, int64_t c, FractionRounding rnd) +{ + // Normalize so that the divisor is positive; the sign is carried by the + // dividend instead. + if (c < 0) { + c = -c; + b = -b; + } + +#if defined(__SIZEOF_INT128__) + // 128-bit intermediate: exact for all 64-bit inputs + __int128 r = __int128(a) * __int128(b); + bool negative = r < 0; + unsigned __int128 ur = negative ? -r : r; + unsigned __int128 uc = static_cast(c); + + unsigned __int128 q; + if (rnd == FractionRounding::kNearInf) { + // Round to nearest, ties away from zero + q = (ur + uc / 2) / uc; + } else { + // Round toward positive infinity + if (!negative) { + q = (ur + uc - 1) / uc; + } else { + q = ur / uc; + } + } + + int64_t res = int64_t(q); + return negative ? -res : res; +#else + // Portable fallback: cross-reduce to keep the intermediate product in + // 64-bit range, then divide with the requested rounding. + int64_t g = i64_gcd(b, c); + if (g) { + b /= g; + c /= g; + } + g = i64_gcd(a, c); + if (g) { + a /= g; + c /= g; + } + + bool negative = (a < 0) != (b < 0); + int64_t ua = a < 0 ? -a : a; + int64_t ub = b < 0 ? -b : b; + + int64_t q; + if (ua != 0 && ub > std::numeric_limits::max() / ua) { + // Extremely unlikely: the product still overflows int64, fall back + // to floating point (may lose precision for huge values). + long double v = (long double)a * (long double)b / (long double)c; + if (rnd == FractionRounding::kNearInf) { + v = v >= 0 ? floorl(v + 0.5L) : ceill(v - 0.5L); + } else { + v = ceill(v); + } + return int64_t(v); + } + + int64_t u = ua * ub; + if (rnd == FractionRounding::kNearInf) { + q = (u + c / 2) / c; + } else { + if (!negative) { + q = (u + c - 1) / c; + } else { + q = u / c; + } + } + + return negative ? -q : q; +#endif +} + +} diff --git a/core/src/util/rational.cpp b/core/src/util/rational.cpp index a0aae381f..f4040638c 100644 --- a/core/src/util/rational.cpp +++ b/core/src/util/rational.cpp @@ -22,7 +22,13 @@ #include "util/rational.h" #include +#include +#include +#include +#include + +#include "util/fractionutils.h" #include "util/stringutils.h" namespace olive::core @@ -39,22 +45,45 @@ rational rational::fromDouble(const double &flt, bool *ok) return NaN; } - // Use FFmpeg function for the time being - AVRational r = av_d2q(flt, INT_MAX); + if (fabs(flt) > double(INT_MAX) + 3.0) { + // Value is out of range for a rational, return NaN + if (ok) { + *ok = false; + } + return NaN; + } - if (r.den == 0) { + // Continued fraction conversion (ported from FFmpeg's av_d2q) + int exponent; + frexp(flt, &exponent); + exponent = std::max(exponent - 1, 0); + int64_t den = 1LL << (62 - exponent); + int64_t num = int64_t(floor(flt * den + 0.5)); + + int64_t rnum = num, rden = den; + ReduceFraction(rnum, rden, INT_MAX); + + if ((!rnum || !rden) && flt) { + // Value was too small to represent above, retry with maximum precision + rnum = int64_t(flt * double(INT64_MAX)); + rden = INT64_MAX; + ReduceFraction(rnum, rden, INT_MAX); + } + + if (rden == 0) { // If den == 0, we were unable to convert to a rational if (ok) { *ok = false; } - } else { - // Otherwise, assume we received a real rational - if (ok) { - *ok = true; - } + return NaN; } - return r; + // Otherwise, assume we received a real rational + if (ok) { + *ok = true; + } + + return rational(int(rnum), int(rden)); } rational rational::fromString(const std::string &str, bool *ok) @@ -80,25 +109,20 @@ rational rational::fromString(const std::string &str, bool *ok) double rational::toDouble() const { - if (r_.den != 0) { - return av_q2d(r_); + if (den_ != 0) { + return double(num_) / double(den_); } else { return std::numeric_limits::quiet_NaN(); } } -AVRational rational::toAVRational() const -{ - return r_; -} - #ifdef USE_OTIO opentime::RationalTime rational::toRationalTime(double framerate) const { // Is this the best way of doing this? // Olive can store rationals as 0/0 which causes errors in OTIO opentime::RationalTime time = - opentime::RationalTime(r_.num, r_.den == 0 ? 1 : r_.den); + opentime::RationalTime(num_, den_ == 0 ? 1 : den_); return time.rescaled_to(framerate); } #endif @@ -113,41 +137,45 @@ rational rational::flipped() const void rational::flip() { if (!isNull()) { - std::swap(r_.den, r_.num); + std::swap(den_, num_); fix_signs(); } } std::string rational::toString() const { - return StringUtils::format("%d/%d", r_.num, r_.den); + return StringUtils::format("%d/%d", num_, den_); } void rational::fix_signs() { - if (r_.den < 0) { + if (den_ < 0) { // Normalize so that denominator is always positive - r_.den = -r_.den; - r_.num = -r_.num; - } else if (r_.den == 0) { + den_ = -den_; + num_ = -num_; + } else if (den_ == 0) { // Normalize to 0/0 (aka NaN) if denominator is zero - r_.num = 0; - } else if (r_.num == 0) { + num_ = 0; + } else if (num_ == 0) { // Normalize to 0/1 if numerator is zero - r_.den = 1; + den_ = 1; } } void rational::reduce() { - av_reduce(&r_.num, &r_.den, r_.num, r_.den, INT_MAX); + int64_t n = num_, d = den_; + ReduceFraction(n, d, INT_MAX); + num_ = int(n); + den_ = int(d); } //Assignment Operators const rational &rational::operator=(const rational &rhs) { - r_ = rhs.r_; + num_ = rhs.num_; + den_ = rhs.den_; return *this; } @@ -160,7 +188,11 @@ const rational &rational::operator+=(const rational &rhs) if (rhs.isNaN()) { *this = NaN; } else { - r_ = av_add_q(r_, rhs.r_); + int64_t n = num_ * int64_t(rhs.den_) + rhs.num_ * int64_t(den_); + int64_t d = den_ * int64_t(rhs.den_); + ReduceFraction(n, d, INT_MAX); + num_ = int(n); + den_ = int(d); fix_signs(); } } @@ -177,7 +209,11 @@ const rational &rational::operator-=(const rational &rhs) if (rhs.isNaN()) { *this = NaN; } else { - r_ = av_sub_q(r_, rhs.r_); + int64_t n = num_ * int64_t(rhs.den_) - rhs.num_ * int64_t(den_); + int64_t d = den_ * int64_t(rhs.den_); + ReduceFraction(n, d, INT_MAX); + num_ = int(n); + den_ = int(d); fix_signs(); } } @@ -194,7 +230,11 @@ const rational &rational::operator*=(const rational &rhs) if (rhs.isNaN()) { *this = NaN; } else { - r_ = av_mul_q(r_, rhs.r_); + int64_t n = num_ * int64_t(rhs.num_); + int64_t d = den_ * int64_t(rhs.den_); + ReduceFraction(n, d, INT_MAX); + num_ = int(n); + den_ = int(d); fix_signs(); } } @@ -211,7 +251,11 @@ const rational &rational::operator/=(const rational &rhs) if (rhs.isNaN()) { *this = NaN; } else { - r_ = av_div_q(r_, rhs.r_); + int64_t n = num_ * int64_t(rhs.den_); + int64_t d = den_ * int64_t(rhs.num_); + ReduceFraction(n, d, INT_MAX); + num_ = int(n); + den_ = int(d); fix_signs(); } } @@ -253,29 +297,29 @@ rational rational::operator*(const rational &rhs) const bool rational::operator<(const rational &rhs) const { - return av_cmp_q(r_, rhs.r_) == -1; + return CompareFractions(num_, den_, rhs.num_, rhs.den_) == -1; } bool rational::operator<=(const rational &rhs) const { - int cmp = av_cmp_q(r_, rhs.r_); + int cmp = CompareFractions(num_, den_, rhs.num_, rhs.den_); return cmp == 0 || cmp == -1; } bool rational::operator>(const rational &rhs) const { - return av_cmp_q(r_, rhs.r_) == 1; + return CompareFractions(num_, den_, rhs.num_, rhs.den_) == 1; } bool rational::operator>=(const rational &rhs) const { - int cmp = av_cmp_q(r_, rhs.r_); + int cmp = CompareFractions(num_, den_, rhs.num_, rhs.den_); return cmp == 0 || cmp == 1; } bool rational::operator==(const rational &rhs) const { - return av_cmp_q(r_, rhs.r_) == 0; + return CompareFractions(num_, den_, rhs.num_, rhs.den_) == 0; } bool rational::operator!=(const rational &rhs) const diff --git a/core/src/util/timecodefunctions.cpp b/core/src/util/timecodefunctions.cpp index 88311f9c2..3a0933eba 100644 --- a/core/src/util/timecodefunctions.cpp +++ b/core/src/util/timecodefunctions.cpp @@ -21,10 +21,10 @@ #include "util/timecodefunctions.h" -extern "C" { -#include -} +#include +#include +#include "util/fractionutils.h" #include "util/stringutils.h" namespace olive::core @@ -334,11 +334,9 @@ rational Timecode::timestamp_to_time(const int64_t ×tamp, int64_t num = int64_t(timebase.numerator()) * timestamp; int64_t den = timebase.denominator(); - int num_r, den_r; + ReduceFraction(num, den, INT_MAX); - av_reduce(&num_r, &den_r, num, den, INT_MAX); - - return rational(num_r, den_r); + return rational(int(num), int(den)); } bool Timecode::timebase_is_drop_frame(const rational &timebase) @@ -389,7 +387,9 @@ int64_t Timecode::rescale_timestamp(const int64_t &ts, const rational &source, return ts; } - return av_rescale_q(ts, source.toAVRational(), dest.toAVRational()); + return RescaleRnd(ts, source.numerator() * int64_t(dest.denominator()), + source.denominator() * int64_t(dest.numerator()), + FractionRounding::kNearInf); } int64_t Timecode::rescale_timestamp_ceil(const int64_t &ts, @@ -400,8 +400,9 @@ int64_t Timecode::rescale_timestamp_ceil(const int64_t &ts, return ts; } - return av_rescale_q_rnd(ts, source.toAVRational(), dest.toAVRational(), - AV_ROUND_UP); + return RescaleRnd(ts, source.numerator() * int64_t(dest.denominator()), + source.denominator() * int64_t(dest.numerator()), + FractionRounding::kUp); } }