Skip to main content

wedeo_core/
rational.rs

1use std::cmp::Ordering;
2use std::fmt;
3use std::ops::{Add, Div, Mul, Neg, Sub};
4
5/// Rounding methods, matching FFmpeg's AVRounding.
6#[derive(Debug, Clone, Copy, PartialEq, Eq)]
7#[repr(u32)]
8pub enum Rounding {
9    /// Round toward zero.
10    Zero = 0,
11    /// Round away from zero.
12    Inf = 1,
13    /// Round toward -infinity.
14    Down = 2,
15    /// Round toward +infinity.
16    Up = 3,
17    /// Round to nearest and halfway cases away from zero.
18    NearInf = 5,
19}
20
21/// Flag telling rescaling functions to pass `i64::MIN`/`i64::MAX` through unchanged.
22pub const ROUND_PASS_MINMAX: u32 = 8192;
23
24/// Rational number (pair of numerator and denominator).
25///
26/// Matches FFmpeg's AVRational. The denominator should not be zero
27/// except for special values (infinity).
28#[derive(Clone, Copy, Default)]
29pub struct Rational {
30    pub num: i32,
31    pub den: i32,
32}
33
34impl Rational {
35    /// Create a new Rational.
36    pub const fn new(num: i32, den: i32) -> Self {
37        Self { num, den }
38    }
39
40    /// Convert to f64.
41    pub fn to_f64(self) -> f64 {
42        self.num as f64 / self.den as f64
43    }
44
45    /// Invert the rational (swap num and den).
46    pub const fn invert(self) -> Self {
47        Self {
48            num: self.den,
49            den: self.num,
50        }
51    }
52
53    /// Reduce the rational to lowest terms with components bounded by `max`.
54    /// Returns `true` if the reduction is exact.
55    pub fn reduce(&mut self, max: i64) -> bool {
56        let (num, den, exact) = reduce(self.num as i64, self.den as i64, max);
57        self.num = num;
58        self.den = den;
59        exact
60    }
61}
62
63impl fmt::Debug for Rational {
64    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
65        write!(f, "{}/{}", self.num, self.den)
66    }
67}
68
69impl fmt::Display for Rational {
70    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
71        write!(f, "{}/{}", self.num, self.den)
72    }
73}
74
75impl PartialEq for Rational {
76    fn eq(&self, other: &Self) -> bool {
77        cmp_q(*self, *other) == Some(Ordering::Equal)
78    }
79}
80
81impl Eq for Rational {}
82
83impl PartialOrd for Rational {
84    fn partial_cmp(&self, other: &Self) -> Option<Ordering> {
85        cmp_q(*self, *other)
86    }
87}
88
89/// Compare two rationals, matching FFmpeg's `av_cmp_q` behavior.
90/// Returns `None` for indeterminate forms (0/0).
91fn cmp_q(a: Rational, b: Rational) -> Option<Ordering> {
92    let tmp = a.num as i64 * b.den as i64 - b.num as i64 * a.den as i64;
93
94    if tmp != 0 {
95        // FFmpeg: return (int)((tmp ^ a.den ^ b.den)>>63)|1;
96        // This yields 1 or -1 depending on the combined sign.
97        let v = ((tmp ^ a.den as i64 ^ b.den as i64) >> 63) | 1;
98        if v > 0 {
99            Some(Ordering::Greater)
100        } else {
101            Some(Ordering::Less)
102        }
103    } else if b.den != 0 && a.den != 0 {
104        Some(Ordering::Equal)
105    } else if a.num != 0 && b.num != 0 {
106        let cmp = (a.num >> 31) - (b.num >> 31);
107        match cmp.cmp(&0) {
108            Ordering::Less => Some(Ordering::Less),
109            Ordering::Greater => Some(Ordering::Greater),
110            Ordering::Equal => Some(Ordering::Equal),
111        }
112    } else {
113        None // indeterminate (0/0)
114    }
115}
116
117/// Reduce a fraction. Returns (num, den, exact).
118/// Matches FFmpeg's `av_reduce`.
119#[allow(unknown_lints, clippy::manual_checked_ops)]
120pub fn reduce(num: i64, den: i64, max: i64) -> (i32, i32, bool) {
121    // Convergents: a0 and a1, each with .num and .den components.
122    // These track the numerator and denominator of successive convergents
123    // of the continued fraction expansion of num/den.
124    let (mut a0n, mut a0d): (u64, u64) = (0, 1);
125    let (mut a1n, mut a1d): (u64, u64) = (1, 0);
126    let sign = (num < 0) ^ (den < 0);
127    let g = gcd(num.unsigned_abs(), den.unsigned_abs());
128    let max = max as u64;
129
130    let mut num = num.unsigned_abs();
131    let mut den = den.unsigned_abs();
132
133    if g != 0 {
134        num /= g;
135        den /= g;
136    }
137
138    // If already within bounds, set a1 = {num, den} and skip the loop
139    // by zeroing den (FFmpeg: den = 0).
140    if num <= max && den <= max {
141        a1n = num;
142        a1d = den;
143        den = 0;
144    }
145
146    while den != 0 {
147        let x = num / den;
148        let next_den = num - den * x;
149        let a2n = x * a1n + a0n;
150        let a2d = x * a1d + a0d;
151
152        if a2n > max || a2d > max {
153            // Last step refinement: find largest k such that
154            // k*a1n + a0n <= max AND k*a1d + a0d <= max
155            let mut x = x;
156            if a1n != 0 {
157                x = (max - a0n) / a1n;
158            }
159            if a1d != 0 {
160                x = x.min((max - a0d) / a1d);
161            }
162
163            // Quality check: is the refined convergent closer to the true value
164            // than the previous convergent a1? If so, accept it.
165            // FFmpeg: if (den * (2*x*a1.den + a0.den) > num * a1.den)
166            if den * (2 * x * a1d + a0d) > num * a1d {
167                a1n = x * a1n + a0n;
168                a1d = x * a1d + a0d;
169            }
170            break;
171        }
172
173        a0n = a1n;
174        a0d = a1d;
175        a1n = a2n;
176        a1d = a2d;
177        num = den;
178        den = next_den;
179    }
180
181    // FFmpeg: exactness is determined by whether den reached 0 naturally
182    // (i.e., the continued fraction terminated without exceeding max).
183    let dst_num = if sign { -(a1n as i32) } else { a1n as i32 };
184    let dst_den = a1d as i32;
185    (dst_num, dst_den, den == 0)
186}
187
188/// Greatest common divisor (Stein's algorithm), matching FFmpeg's `av_gcd`.
189pub fn gcd(mut a: u64, mut b: u64) -> u64 {
190    if a == 0 {
191        return b;
192    }
193    if b == 0 {
194        return a;
195    }
196    let za = a.trailing_zeros();
197    let zb = b.trailing_zeros();
198    let k = za.min(zb);
199    a >>= za;
200    b >>= zb;
201    while a != b {
202        if a > b {
203            std::mem::swap(&mut a, &mut b);
204        }
205        b -= a;
206        b >>= b.trailing_zeros();
207    }
208    a << k
209}
210
211/// Rescale a 64-bit integer with specified rounding.
212/// Mathematically equivalent to `a * b / c`.
213/// Matches FFmpeg's `av_rescale_rnd`.
214pub fn rescale_rnd(a: i64, b: i64, c: i64, rnd: Rounding, pass_minmax: bool) -> i64 {
215    if c <= 0 || b < 0 {
216        return i64::MIN;
217    }
218
219    if pass_minmax && (a == i64::MIN || a == i64::MAX) {
220        return a;
221    }
222
223    if a < 0 {
224        let neg_rnd = match rnd {
225            Rounding::Down => Rounding::Up,
226            Rounding::Up => Rounding::Down,
227            Rounding::Inf => Rounding::Inf,
228            other => other,
229        };
230        let abs_a = if a == i64::MIN { i64::MAX } else { -a };
231        return (rescale_rnd(abs_a, b, c, neg_rnd, false) as u64).wrapping_neg() as i64;
232    }
233
234    let r: i64 = match rnd {
235        Rounding::NearInf => c / 2,
236        Rounding::Inf | Rounding::Up => c - 1,
237        _ => 0,
238    };
239
240    if b <= i32::MAX as i64 && c <= i32::MAX as i64 {
241        if a <= i32::MAX as i64 {
242            return (a * b + r) / c;
243        }
244        let ad = a / c;
245        let a2 = (a % c * b + r) / c;
246        if ad >= i32::MAX as i64 && b != 0 && ad > (i64::MAX - a2) / b {
247            return i64::MIN;
248        }
249        return ad * b + a2;
250    }
251
252    // 128-bit arithmetic path
253    let product = a as u128 * b as u128 + r as u128;
254    let result = product / c as u128;
255    if result > i64::MAX as u128 {
256        return i64::MIN;
257    }
258    result as i64
259}
260
261/// Rescale a 64-bit integer with rounding to nearest.
262/// Matches FFmpeg's `av_rescale`.
263pub fn rescale(a: i64, b: i64, c: i64) -> i64 {
264    rescale_rnd(a, b, c, Rounding::NearInf, false)
265}
266
267/// Rescale a 64-bit integer by 2 rational numbers with specified rounding.
268/// Matches FFmpeg's `av_rescale_q_rnd`.
269pub fn rescale_q_rnd(a: i64, bq: Rational, cq: Rational, rnd: Rounding, pass_minmax: bool) -> i64 {
270    let b = bq.num as i64 * cq.den as i64;
271    let c = cq.num as i64 * bq.den as i64;
272    rescale_rnd(a, b, c, rnd, pass_minmax)
273}
274
275/// Rescale a 64-bit integer by 2 rational numbers.
276/// Matches FFmpeg's `av_rescale_q`.
277pub fn rescale_q(a: i64, bq: Rational, cq: Rational) -> i64 {
278    rescale_q_rnd(a, bq, cq, Rounding::NearInf, false)
279}
280
281impl Neg for Rational {
282    type Output = Self;
283    fn neg(self) -> Self {
284        // Use wrapping_neg to match FFmpeg's C behavior (wraps on i32::MIN).
285        Self {
286            num: self.num.wrapping_neg(),
287            den: self.den,
288        }
289    }
290}
291
292impl Add for Rational {
293    type Output = Self;
294    fn add(self, rhs: Self) -> Self {
295        let num = self.num as i64 * rhs.den as i64 + rhs.num as i64 * self.den as i64;
296        let den = self.den as i64 * rhs.den as i64;
297        let (n, d, _) = reduce(num, den, i32::MAX as i64);
298        Self { num: n, den: d }
299    }
300}
301
302impl Sub for Rational {
303    type Output = Self;
304    fn sub(self, rhs: Self) -> Self {
305        self + (-rhs)
306    }
307}
308
309impl Mul for Rational {
310    type Output = Self;
311    fn mul(self, rhs: Self) -> Self {
312        let num = self.num as i64 * rhs.num as i64;
313        let den = self.den as i64 * rhs.den as i64;
314        let (n, d, _) = reduce(num, den, i32::MAX as i64);
315        Self { num: n, den: d }
316    }
317}
318
319impl Div for Rational {
320    type Output = Self;
321    #[allow(clippy::suspicious_arithmetic_impl)]
322    fn div(self, rhs: Self) -> Self {
323        self * rhs.invert()
324    }
325}
326
327#[cfg(test)]
328mod tests {
329    use super::*;
330
331    #[test]
332    fn test_rational_basics() {
333        let r = Rational::new(1, 2);
334        assert_eq!(r.to_f64(), 0.5);
335        assert_eq!(r.invert(), Rational::new(2, 1));
336    }
337
338    #[test]
339    fn test_rational_arithmetic() {
340        let a = Rational::new(1, 3);
341        let b = Rational::new(1, 6);
342        let sum = a + b;
343        assert_eq!(sum, Rational::new(1, 2));
344
345        let product = Rational::new(2, 3) * Rational::new(3, 4);
346        assert_eq!(product, Rational::new(1, 2));
347    }
348
349    #[test]
350    fn test_rescale_rnd() {
351        // Simple cases
352        assert_eq!(rescale(3, 1, 2), 2); // 3*1/2 = 1.5, round to nearest = 2
353        assert_eq!(rescale_rnd(3, 1, 2, Rounding::Zero, false), 1);
354        assert_eq!(rescale_rnd(3, 1, 2, Rounding::Up, false), 2);
355        assert_eq!(rescale_rnd(3, 1, 2, Rounding::Down, false), 1);
356    }
357
358    #[test]
359    fn test_rescale_q() {
360        let tb1 = Rational::new(1, 48000);
361        let tb2 = Rational::new(1, 1000);
362        // 48000 samples at 48kHz = 1000ms
363        let result = rescale_q(48000, tb1, tb2);
364        assert_eq!(result, 1000);
365    }
366
367    #[test]
368    fn test_gcd() {
369        assert_eq!(gcd(12, 8), 4);
370        assert_eq!(gcd(0, 5), 5);
371        assert_eq!(gcd(5, 0), 5);
372        assert_eq!(gcd(0, 0), 0);
373        assert_eq!(gcd(7, 13), 1);
374    }
375
376    #[test]
377    fn test_reduce() {
378        let (n, d, exact) = reduce(6, 4, i32::MAX as i64);
379        assert!(exact);
380        assert_eq!(n, 3);
381        assert_eq!(d, 2);
382    }
383
384    #[test]
385    fn test_pass_minmax() {
386        assert_eq!(
387            rescale_rnd(i64::MIN, 1, 2, Rounding::NearInf, true),
388            i64::MIN
389        );
390        assert_eq!(
391            rescale_rnd(i64::MAX, 1, 2, Rounding::NearInf, true),
392            i64::MAX
393        );
394    }
395}