diff --git a/.idea/misc.xml b/.idea/misc.xml index 9c3332a..a2e120d 100644 --- a/.idea/misc.xml +++ b/.idea/misc.xml @@ -1,4 +1,4 @@ - + \ No newline at end of file diff --git a/.idea/ptulsconv.iml b/.idea/ptulsconv.iml index 6711606..f3d7bc9 100644 --- a/.idea/ptulsconv.iml +++ b/.idea/ptulsconv.iml @@ -2,7 +2,7 @@ - + diff --git a/ptulsconv/__init__.py b/ptulsconv/__init__.py index 00acfb6..8fbd682 100644 --- a/ptulsconv/__init__.py +++ b/ptulsconv/__init__.py @@ -1,99 +1,39 @@ -from parsimonious.grammar import Grammar - -protools_text_export_grammar = Grammar( - r""" - document = header files_section? clips_section? plugin_listing? track_listing? markers_listing? - header = "SESSION NAME:" fs string_value rs - "SAMPLE RATE:" fs float_value rs - "BIT DEPTH:" fs string_value rs - "SESSION START TIMECODE:" fs timecode_value rs - "TIMECODE FORMAT:" fs float_value " Frame" rs - "# OF AUDIO TRACKS:" fs integer_value rs - "# OF AUDIO CLIPS:" fs integer_value rs - "# OF AUDIO FILES:" fs integer_value rs rs rs - - files_section = files_header files_column_header ( file_record )* rs rs - - files_header = "F I L E S I N S E S S I O N" rs - files_column_header = "Filename " fs "Location" rs - file_record = string_value fs string_value rs - - clips_section = clips_header clips_column_header ( clip_record )* rs rs - clips_header = "O N L I N E C L I P S I N S E S S I O N" rs - clips_column_header = string_value fs string_value rs - clip_record = string_value fs string_value fs "[" integer_value "]" rs - - plugin_listing = plugin_header plugin_column_header ( plugin_record rs )* rs rs - plugin_header = "P L U G - I N S L I S T I N G" rs - plugin_column_header = "MANUFACTURER " fs "PLUG-IN NAME " fs - "VERSION " fs "FORMAT " fs "STEMS " fs - "NUMBER OF INSTANCES" rs - plugin_record = string_value fs string_value fs string_value fs - string_value fs string_value fs string_value rs - - - track_listing = track_listing_header ( track_list )* - track_listing_header = "T R A C K L I S T I N G" rs - - track_list = "TRACK NAME:" fs string_value rs - "COMMENTS:" fs string_value rs - "USER DELAY:" fs integer_value " Samples" rs - "STATE: " ( fs string_value )* rs - "PLUG-INS: " ( fs string_value )* rs - track_clip_list rs rs - - track_clip_list = "CHANNEL " fs "EVENT " fs "CLIP NAME " fs - "START TIME " fs "END TIME " fs "DURATION " fs "STATE" rs - (track_clip_entry)* - - track_clip_entry = integer_value isp fs - integer_value isp fs - string_value fs - timecode_value fs timecode_value fs timecode_value fs - track_clip_state rs - track_clip_state = ("Muted" / "Unmuted") - - markers_listing = markers_listing_header markers_column_header marker_record* - markers_listing_header = "M A R K E R S L I S T I N G" rs - markers_column_header = "# " fs "LOCATION " fs "TIME REFERENCE " fs - "UNITS " fs "NAME " fs "COMMENTS" rs - - marker_record = string_value fs string_value fs string_value fs - string_value fs string_value fs string_value rs - - fs = "\t" - rs = "\n" - string_value = ~"[^\S\t\n]*" ~"[^\t\n]*" - timecode_value = ~"[^\d\t\n]*" ~"\d\d" ":" ~"\d\d" ":" ~"\d\d" ":" ~"\d\d" ~"[^\d\t\n]*" - integer_value = ~"\d+" - float_value = ~"\d+(\.\d+)" - isp = ~"[^\d\t\n]*" - """) - -from parsimonious.nodes import NodeVisitor, Node -from timecode import Timecode +from .ptuls_grammar import protools_text_export_grammar +from parsimonious.nodes import NodeVisitor class PTTextVisitor(NodeVisitor): def visit_document(self, node, visited_children): - return {'header': visited_children[0], - 'files': visited_children[1][0], - 'clips': visited_children[2][0], - 'plugins': visited_children[3][0], - 'tracks': visited_children[4][0] - } + files = None + clips = None + plugins = None + tracks = None + markers = None + if isinstance(visited_children[1] ,list): + files = visited_children[1][0] + if isinstance(visited_children[2], list): + clips = visited_children[2][0] + if isinstance(visited_children[3], list): + plugins = visited_children[3][0] + if isinstance(visited_children[4], list): + tracks = visited_children[4][0] + + return dict(header=visited_children[0], + files=files, + clips=clips, + plugins=plugins, + tracks=tracks, + markers=markers) def visit_header(self, node, visited_children): - return { - 'session_name': visited_children[2], - 'sample_rate': visited_children[6], - 'bit_depth': visited_children[10], - 'start_timecode': visited_children[14], - 'timecode_format': visited_children[18], - 'count_audio_tracks': visited_children[23], - 'count_clips': visited_children[27], - 'count_files': visited_children[31] - } + return dict(session_name=visited_children[2], + sample_rate=visited_children[6], + bit_depth=visited_children[10], + start_timecode=visited_children[15], + timecode_format=visited_children[19], + count_audio_tracks=visited_children[24], + count_clips=visited_children[28], + count_files=visited_children[32]) def visit_files_section(self, node, visited_children): return list(map(lambda child: {'filename': child[0], 'path': child[2]}, visited_children[2])) @@ -108,32 +48,45 @@ class PTTextVisitor(NodeVisitor): 'count_instances': child[10]}, visited_children[2])) - def visit_track_listing(self, node, visited_children): - retval = [] - for child in visited_children[1]: - state = list(map(lambda t: t.text, child[14])) - plugs = list(map(lambda t: t.text, child[17])) - retval.append({'track_name': child[2], - 'comments': child[6], - 'samples_delay': child[10], - 'state': state, - 'clips': child[19]}) + def visit_track_block(self, node, visited_children): + clips = [] + for clip in visited_children[1]: + if clip[0] != None: + clips.append(clip[0]) - return retval + return dict( + name=visited_children[0][2], + comments=visited_children[0][6], + user_delay_samples=visited_children[0][10], + state=visited_children[0][14], + clips=clips + ) - def visit_track_clip_list(self, node, visited_children): - return visited_children[14] + def visit_track_listing(selfs, node, visited_children): + return visited_children[1] def visit_track_clip_entry(self, node, visited_children): + timestamp = None + if isinstance(visited_children[14], list): + timestamp = visited_children[14][0][0] + return {'channel': visited_children[0], 'event': visited_children[3], 'clip_name': visited_children[6], 'start_time': visited_children[8], 'end_time': visited_children[10], 'duration': visited_children[12], - 'state': visited_children[14] + 'timestamp' : timestamp, + 'state': visited_children[15] } + def visit_track_state_list(self, node, visited_children): + states = [] + for next_state in visited_children: + states.append(next_state[0][0].text) + + return states + def visit_track_clip_state(self, node, visited_children): return node.text @@ -144,20 +97,20 @@ class PTTextVisitor(NodeVisitor): return visited_children[1].text def visit_string_value(self, node, visited_children): - return visited_children[1].text + return node.text.strip(" ") def visit_integer_value(self, node, visited_children): return int(node.text) def visit_timecode_value(self, node, visited_children): - return visited_children[1].text + visited_children[2].text + \ - visited_children[3].text + visited_children[4].text + \ - visited_children[5].text + visited_children[6].text + \ - visited_children[7].text + return visited_children[1].text def visit_float_value(self, node, visited_children): return float(node.text) + def visit_block_ending(self, node, visited_children): + pass + def generic_visit(self, node, visited_children): """ The generic visit method. """ return visited_children or node diff --git a/ptulsconv/ptuls_grammar.py b/ptulsconv/ptuls_grammar.py new file mode 100644 index 0000000..0c5fa35 --- /dev/null +++ b/ptulsconv/ptuls_grammar.py @@ -0,0 +1,74 @@ +from parsimonious.grammar import Grammar + +protools_text_export_grammar = Grammar( + r""" + document = header files_section? clips_section? plugin_listing? track_listing? markers_listing? + header = "SESSION NAME:" fs string_value rs + "SAMPLE RATE:" fs float_value rs + "BIT DEPTH:" fs integer_value "-bit" rs + "SESSION START TIMECODE:" fs timecode_value rs + "TIMECODE FORMAT:" fs float_value " Frame" rs + "# OF AUDIO TRACKS:" fs integer_value rs + "# OF AUDIO CLIPS:" fs integer_value rs + "# OF AUDIO FILES:" fs integer_value rs block_ending + + files_section = files_header files_column_header file_record* block_ending + files_header = "F I L E S I N S E S S I O N" rs + files_column_header = "Filename " fs "Location" rs + file_record = string_value fs string_value rs + + clips_section = clips_header clips_column_header clip_record* block_ending + clips_header = "O N L I N E C L I P S I N S E S S I O N" rs + clips_column_header = string_value fs string_value rs + clip_record = string_value fs string_value fs "[" integer_value "]" rs + + plugin_listing = plugin_header plugin_column_header plugin_record* block_ending + plugin_header = "P L U G - I N S L I S T I N G" rs + plugin_column_header = "MANUFACTURER " fs "PLUG-IN NAME " fs + "VERSION " fs "FORMAT " fs "STEMS " fs + "NUMBER OF INSTANCES" rs + plugin_record = string_value fs string_value fs string_value fs + string_value fs string_value fs string_value rs + + track_listing = track_listing_header track_block* + track_block = track_list_top ( track_clip_entry / block_ending )* + + track_listing_header = "T R A C K L I S T I N G" rs + track_list_top = "TRACK NAME:" fs string_value rs + "COMMENTS:" fs string_value rs + "USER DELAY:" fs integer_value " Samples" rs + "STATE: " track_state_list rs + "PLUG-INS: " ( fs string_value )* rs + "CHANNEL " fs "EVENT " fs "CLIP NAME " fs + "START TIME " fs "END TIME " fs "DURATION " fs + ("TIMESTAMP " fs)? "STATE" rs + + track_state_list = (track_state " ")* + + track_state = "Solo" / "Muted" / "Inactive" + + track_clip_entry = integer_value isp fs + integer_value isp fs + string_value fs + timecode_value fs timecode_value fs timecode_value fs (timecode_value fs)? + track_clip_state rs + + track_clip_state = ("Muted" / "Unmuted") + + markers_listing = markers_listing_header markers_column_header marker_record* + markers_listing_header = "M A R K E R S L I S T I N G" rs + markers_column_header = "# " fs "LOCATION " fs "TIME REFERENCE " fs + "UNITS " fs "NAME " fs "COMMENTS" rs + + marker_record = string_value fs string_value fs string_value fs + string_value fs string_value fs string_value rs + + fs = "\t" + rs = "\n" + block_ending = rs rs + string_value = ~"[^\t\n]*" + timecode_value = ~"[^\d\t\n]*" ~"\d\d:\d\d:\d\d:\d\d(\.\d+)?" ~"[^\d\t\n]*" + integer_value = ~"\d+" + float_value = ~"\d+(\.\d+)" + isp = ~"[^\d\t\n]*" + """) diff --git a/tests/test_robinhood1.py b/tests/test_robinhood1.py new file mode 100644 index 0000000..b947c17 --- /dev/null +++ b/tests/test_robinhood1.py @@ -0,0 +1,79 @@ +import unittest +import ptulsconv +import pprint + +class TestRobinHood1(unittest.TestCase): + path = 'tests/export_cases/Robin Hood Spotting.txt' + + def test_header_export(self): + with open(self.path, 'r') as f: + visitor = ptulsconv.PTTextVisitor() + result = ptulsconv.protools_text_export_grammar.parse(f.read()) + parsed :dict = visitor.visit(result) + + self.assertTrue('header' in parsed.keys()) + self.assertEqual(parsed['header']['session_name'], 'Robin Hood Spotting') + self.assertEqual(parsed['header']['sample_rate'], 48000.0) + self.assertEqual(parsed['header']['bit_depth'], 24) + self.assertEqual(parsed['header']['timecode_format'], 29.97) + + def test_all_sections(self): + with open(self.path, 'r') as f: + visitor = ptulsconv.PTTextVisitor() + result = ptulsconv.protools_text_export_grammar.parse(f.read()) + parsed :dict = visitor.visit(result) + + self.assertIn('header', parsed.keys()) + self.assertIn('files', parsed.keys()) + self.assertIn('clips', parsed.keys()) + self.assertIn('plugins', parsed.keys()) + self.assertIn('tracks', parsed.keys()) + self.assertIn('markers', parsed.keys()) + + def test_tracks(self): + with open(self.path, 'r') as f: + visitor = ptulsconv.PTTextVisitor() + result = ptulsconv.protools_text_export_grammar.parse(f.read()) + parsed :dict = visitor.visit(result) + self.assertEqual(len(parsed['tracks']), 14) + self.assertListEqual(["Scenes", "Robin", "Will", "Marian", "John", + "Guy", "Much", "Butcher", "Town Crier", + "Soldier 1", "Soldier 2", "Soldier 3", + "Priest", "Guest at Court"], + list(map(lambda n: n['name'], parsed['tracks']))) + self.assertListEqual(["", "[ADR] {Actor=Errol Flynn} $CN=1", + "[ADR] {Actor=Patrick Knowles} $CN=2", + "[ADR] {Actor=Olivia DeHavilland} $CN=3", + "[ADR] {Actor=Claude Raines} $CN=4", + "[ADR] {Actor=Basil Rathbone} $CN=5", + "[ADR] {Actor=Herbert Mundin} $CN=6", + "[ADR] {Actor=George Bunny} $CN=101", + "[ADR] {Actor=Leonard Mundie} $CN=102", + "[ADR] $CN=103", + "[ADR] $CN=104", + "[ADR] $CN=105", + "[ADR] {Actor=Thomas R. Mills} $CN=106", + "[ADR] $CN=107"], + list(map(lambda n: n['comments'], parsed['tracks']))) + + def test_a_track(self): + with open(self.path, 'r') as f: + visitor = ptulsconv.PTTextVisitor() + result = ptulsconv.protools_text_export_grammar.parse(f.read()) + parsed :dict = visitor.visit(result) + guy_track = parsed['tracks'][5] + self.assertEqual(guy_track['name'], 'Guy') + self.assertEqual(guy_track['comments'], '[ADR] {Actor=Basil Rathbone} $CN=5') + self.assertEqual(guy_track['user_delay_samples'], 0) + self.assertListEqual(guy_track['state'], []) + self.assertEqual(len(guy_track['clips']), 16) + self.assertEqual(guy_track['clips'][5]['channel'], 1) + self.assertEqual(guy_track['clips'][5]['event'], 6) + self.assertEqual(guy_track['clips'][5]['clip_name'], "\"What's your name? You Saxon dog!\" $QN=GY106" ) + self.assertEqual(guy_track['clips'][5]['start_time'], "01:04:19:15") + self.assertEqual(guy_track['clips'][5]['end_time'], "01:04:21:28") + self.assertEqual(guy_track['clips'][5]['duration'], "00:00:02:13") + self.assertEqual(guy_track['clips'][5]['state'], 'Unmuted') + +if __name__ == '__main__': + unittest.main() diff --git a/tests/test_robinhood5.py b/tests/test_robinhood5.py new file mode 100644 index 0000000..ab0e213 --- /dev/null +++ b/tests/test_robinhood5.py @@ -0,0 +1,49 @@ +import unittest +import ptulsconv +from pprint import pprint + +class TestRobinHood5(unittest.TestCase): + path = 'tests/export_cases/Robin Hood Spotting5.txt' + + def test_plugins(self): + with open(self.path, 'r') as f: + visitor = ptulsconv.PTTextVisitor() + result = ptulsconv.protools_text_export_grammar.parse(f.read()) + parsed: dict = visitor.visit(result) + self.assertEqual(len(parsed['plugins']), 2) + + def test_stereo_track(self): + with open(self.path, 'r') as f: + visitor = ptulsconv.PTTextVisitor() + result = ptulsconv.protools_text_export_grammar.parse(f.read()) + parsed: dict = visitor.visit(result) + self.assertEqual(parsed['tracks'][1]['name'], 'MX WT (Stereo)') + self.assertEqual(len(parsed['tracks'][1]['clips']), 2) + self.assertEqual(parsed['tracks'][1]['clips'][0]['clip_name'], 'RobinHood.1-01.L') + self.assertEqual(parsed['tracks'][1]['clips'][1]['clip_name'], 'RobinHood.1-01.R') + + def test_a_track(self): + with open(self.path, 'r') as f: + visitor = ptulsconv.PTTextVisitor() + result = ptulsconv.protools_text_export_grammar.parse(f.read()) + parsed :dict = visitor.visit(result) + + guy_track = parsed['tracks'][8] + self.assertEqual(guy_track['name'], 'Guy') + self.assertEqual(guy_track['comments'], '[ADR] {Actor=Basil Rathbone} $CN=5') + self.assertEqual(guy_track['user_delay_samples'], 0) + self.assertListEqual(guy_track['state'], ['Solo']) + self.assertEqual(len(guy_track['clips']), 16) + self.assertEqual(guy_track['clips'][5]['channel'], 1) + self.assertEqual(guy_track['clips'][5]['event'], 6) + self.assertEqual(guy_track['clips'][5]['clip_name'], "\"What's your name? You Saxon dog!\" $QN=GY106" ) + self.assertEqual(guy_track['clips'][5]['start_time'], "01:04:19:15.00") + self.assertEqual(guy_track['clips'][5]['end_time'], "01:04:21:28.00") + self.assertEqual(guy_track['clips'][5]['duration'], "00:00:02:13.00") + self.assertEqual(guy_track['clips'][5]['timestamp'], "01:04:19:09.70") + self.assertEqual(guy_track['clips'][5]['state'], 'Unmuted') + + + +if __name__ == '__main__': + unittest.main()