From 145f7b2982a1151abc77b9f4f6d9369cc3acca0a Mon Sep 17 00:00:00 2001 From: Jamie Hardt Date: Tue, 8 Oct 2019 09:25:45 -0700 Subject: [PATCH] Timecode implementation --- .idea/dictionaries/jamiehardt.xml | 8 ++++ ptulsconv/broadcast_timecode.py | 74 +++++++++++++++++++++++++++++++ tests/test_broadcast_timecode.py | 69 ++++++++++++++++++++++++++++ 3 files changed, 151 insertions(+) create mode 100644 .idea/dictionaries/jamiehardt.xml create mode 100644 ptulsconv/broadcast_timecode.py create mode 100644 tests/test_broadcast_timecode.py diff --git a/.idea/dictionaries/jamiehardt.xml b/.idea/dictionaries/jamiehardt.xml new file mode 100644 index 0000000..d6975d7 --- /dev/null +++ b/.idea/dictionaries/jamiehardt.xml @@ -0,0 +1,8 @@ + + + + frac + mins + + + \ No newline at end of file diff --git a/ptulsconv/broadcast_timecode.py b/ptulsconv/broadcast_timecode.py new file mode 100644 index 0000000..e6593d9 --- /dev/null +++ b/ptulsconv/broadcast_timecode.py @@ -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) + + + + diff --git a/tests/test_broadcast_timecode.py b/tests/test_broadcast_timecode.py new file mode 100644 index 0000000..4762e3c --- /dev/null +++ b/tests/test_broadcast_timecode.py @@ -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()