|
91 | 91 | MAX_FPS_DELTA: float = 1.0 / 1000000000.0 |
92 | 92 | """Maximum amount two framerates can differ by for equality testing. Currently 1 frame/nanosec.""" |
93 | 93 |
|
| 94 | +# `datetime.timedelta` does not expose seconds per minute/hour as constants, so we define our own. |
94 | 95 | _SECONDS_PER_MINUTE = 60.0 |
95 | 96 | _SECONDS_PER_HOUR = 60.0 * _SECONDS_PER_MINUTE |
96 | 97 | _MINUTES_PER_HOUR = 60.0 |
97 | 98 |
|
98 | | -# Common framerates mapped from their float representation to exact rational values. |
99 | | -_COMMON_FRAMERATES: dict[Fraction, Fraction] = { |
100 | | - Fraction(24000, 1001): Fraction(24000, 1001), # 23.976... |
101 | | - Fraction(30000, 1001): Fraction(30000, 1001), # 29.97... |
102 | | - Fraction(60000, 1001): Fraction(60000, 1001), # 59.94... |
103 | | - Fraction(120000, 1001): Fraction(120000, 1001), # 119.88... |
104 | | -} |
| 99 | +# Tolerance for snapping a float value's framerate to an NTSC-derived rational (N * 1000/1001). |
| 100 | +# e.g. 23.976 should be detected as 24000/1001, 29.97 should be detected as 30000/1001, etc. |
| 101 | +_NTSC_DETECTION_TOLERANCE: float = 1e-3 |
105 | 102 |
|
106 | 103 |
|
107 | 104 | def framerate_to_fraction(fps: float) -> Fraction: |
108 | 105 | """Convert a float framerate to an exact rational Fraction. |
109 | 106 |
|
110 | | - Recognizes common NTSC framerates (23.976, 29.97, 59.94, 119.88) and maps them to their |
111 | | - exact rational representation (e.g. 24000/1001). For other values, uses limit_denominator |
112 | | - to find a clean rational approximation, or returns the exact integer fraction for whole |
113 | | - number framerates. |
| 107 | + Detects NTSC-derived framerates of the form ``N * 1000/1001`` (e.g. 23.976 -> 24000/1001, |
| 108 | + 29.97 -> 30000/1001, 47.952 -> 48000/1001) for any positive integer ``N`` and returns |
| 109 | + their exact rational representation. Whole-number framerates are returned as |
| 110 | + ``Fraction(N, 1)``. Other values fall back to ``limit_denominator(10000)`` for a clean |
| 111 | + rational approximation. |
114 | 112 | """ |
115 | 113 | if fps <= MAX_FPS_DELTA: |
116 | 114 | raise ValueError("Framerate must be positive and greater than zero.") |
117 | | - # Integer framerates are exact. |
118 | 115 | if fps == int(fps): |
119 | 116 | return Fraction(int(fps), 1) |
120 | | - # Check against known common framerates using limit_denominator to find the closest match. |
121 | | - candidate = Fraction(fps).limit_denominator(10000) |
122 | | - if candidate in _COMMON_FRAMERATES: |
123 | | - return _COMMON_FRAMERATES[candidate] |
124 | | - return candidate |
| 117 | + # Invert fps = N * 1000/1001 to recover N, then verify within tolerance. |
| 118 | + base = round(fps * 1001 / 1000) |
| 119 | + if base > 0 and abs(base * 1000 / 1001 - fps) < _NTSC_DETECTION_TOLERANCE: |
| 120 | + return Fraction(base * 1000, 1001) |
| 121 | + return Fraction(fps).limit_denominator(10000) |
125 | 122 |
|
126 | 123 |
|
127 | 124 | class Interpolation(Enum): |
|
0 commit comments