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()