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).
This commit is contained in:
2026-07-15 21:55:11 +08:00
parent 3be4294e15
commit 0f41620a0b
10 changed files with 480 additions and 287 deletions
+5 -87
View File
@@ -40,16 +40,15 @@ const std::vector<int> AudioParams::kSupportedSampleRates = {
};
const std::vector<uint64_t> 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_);
}
}
+1 -4
View File
@@ -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);
+205
View File
@@ -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 <http://www.gnu.org/licenses/>.
***/
#include "util/fractionutils.h"
#include <limits.h>
#include <math.h>
#include <stdlib.h>
#include <limits>
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<unsigned __int128>(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<int64_t>::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
}
}
+81 -37
View File
@@ -22,7 +22,13 @@
#include "util/rational.h"
#include <math.h>
#include <stdint.h>
#include <algorithm>
#include <climits>
#include <limits>
#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<double>::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
+11 -10
View File
@@ -21,10 +21,10 @@
#include "util/timecodefunctions.h"
extern "C" {
#include <libavutil/mathematics.h>
}
#include <climits>
#include <cmath>
#include "util/fractionutils.h"
#include "util/stringutils.h"
namespace olive::core
@@ -334,11 +334,9 @@ rational Timecode::timestamp_to_time(const int64_t &timestamp,
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);
}
}