mirror of
https://github.com/iluvcapra/ptulsconv.git
synced 2025-12-31 08:50:48 +00:00
Timecode implementation
This commit is contained in:
8
.idea/dictionaries/jamiehardt.xml
generated
Normal file
8
.idea/dictionaries/jamiehardt.xml
generated
Normal file
@@ -0,0 +1,8 @@
|
||||
<component name="ProjectDictionaryState">
|
||||
<dictionary name="jamiehardt">
|
||||
<words>
|
||||
<w>frac</w>
|
||||
<w>mins</w>
|
||||
</words>
|
||||
</dictionary>
|
||||
</component>
|
||||
74
ptulsconv/broadcast_timecode.py
Normal file
74
ptulsconv/broadcast_timecode.py
Normal file
@@ -0,0 +1,74 @@
|
||||
from fractions import Fraction
|
||||
import re
|
||||
import math
|
||||
import functools
|
||||
|
||||
|
||||
def smpte_to_frame_count(smpte_rep_string: str, frames_per_logical_second: int, drop_frame_hint=False):
|
||||
"""
|
||||
Convert a string with a SMPTE timecode representation into a frame count.
|
||||
|
||||
:param smpte_rep_string: The timecode string
|
||||
:param frames_per_logical_second: Num of frames in a logical second. This is asserted to be
|
||||
in one of `[24,25,30,48,50,60]`
|
||||
:param drop_frame_hint: `True` if the timecode rep is drop frame. This is ignored (and implied `True`) if
|
||||
the last separator in the timecode string is a semicolon. This is ignored (and implied `False`) if
|
||||
`frames_per_logical_second` is not 30 or 60.
|
||||
:returns (frame_count, fraction): If a fractional frame is in the SMPTE string it will be returned here in the
|
||||
`fraction` part.
|
||||
"""
|
||||
assert frames_per_logical_second in [24, 25, 30, 48, 50, 60]
|
||||
|
||||
m = re.search("(\d?\d)[:;](\d\d)[:;](\d\d)([:;])(\d\d)(\.\d+)?", smpte_rep_string)
|
||||
hh, mm, ss, sep, ff, frac = m.groups()
|
||||
hh, mm, ss, ff = int(hh), int(mm), int(ss), int(ff)
|
||||
if frac is not None:
|
||||
frac = float(frac)
|
||||
|
||||
drop_frame = drop_frame_hint
|
||||
if sep == ";":
|
||||
drop_frame = True
|
||||
|
||||
if frames_per_logical_second not in [30, 60]:
|
||||
drop_frame = False
|
||||
|
||||
raw_frames = hh * 3600 * frames_per_logical_second + mm * 60 * frames_per_logical_second + \
|
||||
ss * frames_per_logical_second + ff
|
||||
|
||||
if drop_frame is False:
|
||||
return raw_frames, frac
|
||||
else:
|
||||
frames_dropped_per_inst = (frames_per_logical_second / 15)
|
||||
mins = hh * 60 + mm
|
||||
inst_count = mins - math.floor(mins / 10)
|
||||
dropped_frames = frames_dropped_per_inst * inst_count
|
||||
return raw_frames - dropped_frames, frac
|
||||
|
||||
|
||||
def frame_count_to_smpte(frame_count: int, frames_per_logical_second: int, drop_frame: bool = False,
|
||||
fractional_frame: float = None):
|
||||
assert frames_per_logical_second in [24,25,30,48,50,60]
|
||||
nominal_frames = frame_count
|
||||
separator = ":"
|
||||
if drop_frame:
|
||||
assert frames_per_logical_second in [30, 60]
|
||||
mins , _= divmod(nominal_frames, frames_per_logical_second * 60)
|
||||
frames_dropped_per_inst = (frames_per_logical_second / 15)
|
||||
inst_count = mins - math.floor(mins / 10)
|
||||
dropped_frames = frames_dropped_per_inst * inst_count
|
||||
nominal_frames = nominal_frames + dropped_frames
|
||||
separator = ";"
|
||||
|
||||
hh, rem = divmod(nominal_frames, frames_per_logical_second * 3600)
|
||||
mm, rem = divmod(rem, frames_per_logical_second * 60)
|
||||
ss, ff = divmod(rem, frames_per_logical_second)
|
||||
|
||||
hh = hh % 24
|
||||
if fractional_frame is not None and fractional_frame > 0:
|
||||
return "%02i:%02i:%02i%s%02i%s" % (hh, mm, ss, separator, ff, ("%.3f" % fractional_frame)[1:])
|
||||
else:
|
||||
return "%02i:%02i:%02i%s%02i" % (hh, mm, ss, separator, ff)
|
||||
|
||||
|
||||
|
||||
|
||||
69
tests/test_broadcast_timecode.py
Normal file
69
tests/test_broadcast_timecode.py
Normal file
@@ -0,0 +1,69 @@
|
||||
import unittest
|
||||
from ptulsconv import broadcast_timecode
|
||||
|
||||
class TestBroadcastTimecode(unittest.TestCase):
|
||||
def test_basic_to_framecount(self):
|
||||
r1 = "01:00:00:00"
|
||||
f1, _ = broadcast_timecode.smpte_to_frame_count(r1, 24, False)
|
||||
self.assertEqual(f1, 86_400)
|
||||
f2, _ = broadcast_timecode.smpte_to_frame_count(r1, 30)
|
||||
self.assertEqual(f2, 108_000)
|
||||
|
||||
r2 = "0:00:00:01"
|
||||
f3, _ = broadcast_timecode.smpte_to_frame_count(r2, 24)
|
||||
self.assertEqual(f3, 1)
|
||||
|
||||
def test_basic_to_string(self):
|
||||
c1 = 24
|
||||
s1 = broadcast_timecode.frame_count_to_smpte(c1,24)
|
||||
self.assertEqual(s1, "00:00:01:00")
|
||||
s2 = broadcast_timecode.frame_count_to_smpte(c1, 30)
|
||||
self.assertEqual(s2, "00:00:00:24")
|
||||
c2 = 108_000
|
||||
s3 = broadcast_timecode.frame_count_to_smpte(c2, 30)
|
||||
self.assertEqual(s3, "01:00:00:00")
|
||||
c3 = 86401
|
||||
s4 = broadcast_timecode.frame_count_to_smpte(c3, 24)
|
||||
self.assertEqual(s4, "01:00:00:01")
|
||||
|
||||
def test_drop_frame_to_string(self):
|
||||
c1 = 108_000
|
||||
s1 = broadcast_timecode.frame_count_to_smpte(c1, 30, drop_frame=True)
|
||||
self.assertEqual(s1, "01:00:03;18")
|
||||
|
||||
def test_fractional_to_framecount(self):
|
||||
s1 = "00:00:01:04.105"
|
||||
c1, f1 = broadcast_timecode.smpte_to_frame_count(s1, 24, drop_frame_hint=False)
|
||||
self.assertEqual(c1, 28)
|
||||
self.assertEqual(f1, 0.105)
|
||||
|
||||
def test_fractional_to_string(self):
|
||||
c1 = 99
|
||||
f1 = .145
|
||||
s1 = broadcast_timecode.frame_count_to_smpte(c1, 25, drop_frame=False, fractional_frame=f1)
|
||||
self.assertEqual(s1, "00:00:03:24.145")
|
||||
|
||||
def test_drop_frame_to_framecount(self):
|
||||
r1 = "01:00:00;00"
|
||||
f1, _ = broadcast_timecode.smpte_to_frame_count(r1, 30, True)
|
||||
self.assertEqual(f1, 107_892)
|
||||
|
||||
r11 = "01:00:00;01"
|
||||
f11, _ = broadcast_timecode.smpte_to_frame_count(r11, 30, True)
|
||||
self.assertEqual(f11, 107_893)
|
||||
|
||||
r2 = "00:01:00;02"
|
||||
f2, _ = broadcast_timecode.smpte_to_frame_count(r2, 30, True)
|
||||
self.assertEqual(f2, 1800)
|
||||
|
||||
r3 = "00:00:59;29"
|
||||
f3, _ = broadcast_timecode.smpte_to_frame_count(r3, 30, True)
|
||||
self.assertEqual(f3, 1799)
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
if __name__ == '__main__':
|
||||
unittest.main()
|
||||
Reference in New Issue
Block a user