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
+19 -117
View File
@@ -23,13 +23,10 @@
#define LIBOLIVECORE_AUDIOPARAMS_H
#include <cstring>
extern "C" {
#include <libavutil/channel_layout.h>
}
#include <assert.h>
#include <vector>
#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<AudioParams> 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<uint64_t> kSupportedChannelLayouts;
static const std::vector<int> 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
@@ -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 <http://www.gnu.org/licenses/>.
***/
#ifndef LIBOLIVECORE_CHANNELLAYOUT_H
#define LIBOLIVECORE_CHANNELLAYOUT_H
#include <stdint.h>
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
@@ -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 <http://www.gnu.org/licenses/>.
***/
#ifndef LIBOLIVECORE_FRACTIONUTILS_H
#define LIBOLIVECORE_FRACTIONUTILS_H
#include <stdint.h>
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
+14 -25
View File
@@ -22,10 +22,7 @@
#ifndef LIBOLIVECORE_RATIONAL_H
#define LIBOLIVECORE_RATIONAL_H
extern "C" {
#include <libavutil/rational.h>
}
#include <climits>
#include <iostream>
#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)