diff --git a/.idea/.gitignore b/.idea/.gitignore
new file mode 100644
index 0000000..a566bfe
--- /dev/null
+++ b/.idea/.gitignore
@@ -0,0 +1,4 @@
+
+# Default ignored files
+/workspace.xml
+/tasks.xml
diff --git a/.idea/.gitignore.swp b/.idea/.gitignore.swp
new file mode 100644
index 0000000..b0a8a29
Binary files /dev/null and b/.idea/.gitignore.swp differ
diff --git a/.idea/inspectionProfiles/profiles_settings.xml b/.idea/inspectionProfiles/profiles_settings.xml
new file mode 100644
index 0000000..105ce2d
--- /dev/null
+++ b/.idea/inspectionProfiles/profiles_settings.xml
@@ -0,0 +1,6 @@
+
+
+
+
+
+
\ No newline at end of file
diff --git a/.idea/misc.xml b/.idea/misc.xml
new file mode 100644
index 0000000..9c3332a
--- /dev/null
+++ b/.idea/misc.xml
@@ -0,0 +1,4 @@
+
+
+
+
\ No newline at end of file
diff --git a/.idea/modules.xml b/.idea/modules.xml
new file mode 100644
index 0000000..922e7b3
--- /dev/null
+++ b/.idea/modules.xml
@@ -0,0 +1,8 @@
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/.idea/ptulsconv.iml b/.idea/ptulsconv.iml
new file mode 100644
index 0000000..6711606
--- /dev/null
+++ b/.idea/ptulsconv.iml
@@ -0,0 +1,11 @@
+
+
+
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/.idea/vcs.xml b/.idea/vcs.xml
new file mode 100644
index 0000000..94a25f7
--- /dev/null
+++ b/.idea/vcs.xml
@@ -0,0 +1,6 @@
+
+
+
+
+
+
\ No newline at end of file
diff --git a/ptulsconv/__init__.py b/ptulsconv/__init__.py
new file mode 100644
index 0000000..00acfb6
--- /dev/null
+++ b/ptulsconv/__init__.py
@@ -0,0 +1,163 @@
+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
+
+
+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]
+ }
+
+ 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]
+ }
+
+ def visit_files_section(self, node, visited_children):
+ return list(map(lambda child: {'filename': child[0], 'path': child[2]}, visited_children[2]))
+
+ def visit_clips_section(self, node, visited_children):
+ return list(map(lambda child: {'clip_name': child[0], 'file': child[2], 'channel': child[5]},
+ visited_children[2]))
+
+ def visit_plugin_listing(self, node, visited_children):
+ return list(map(lambda child: {'manufacturer': child[0], 'plugin_name': child[2],
+ 'version': child[4], 'format': child[6], 'stems': child[8],
+ '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]})
+
+ return retval
+
+ def visit_track_clip_list(self, node, visited_children):
+ return visited_children[14]
+
+ def visit_track_clip_entry(self, node, visited_children):
+ 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]
+ }
+
+ def visit_track_clip_state(self, node, visited_children):
+ return node.text
+
+ def visit_markers_listing(self, node, visited_children):
+ return 'Markers'
+
+ def visit_formatted_clip_name(self, node, visited_children):
+ return visited_children[1].text
+
+ def visit_string_value(self, node, visited_children):
+ return visited_children[1].text
+
+ 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
+
+ def visit_float_value(self, node, visited_children):
+ return float(node.text)
+
+ def generic_visit(self, node, visited_children):
+ """ The generic visit method. """
+ return visited_children or node