├── .gitignore ├── LICENSE ├── README.md ├── __init__.py ├── parsers ├── __init__.py ├── audio_mixer.py ├── audio_synth_folder.py ├── media_pool.py ├── mixer_console.py ├── music_track_device.py ├── song.py └── song_parser.py └── song_model.py /.gitignore: -------------------------------------------------------------------------------- 1 | .DS_Store 2 | *~ 3 | *.pyc 4 | *.app 5 | /build 6 | /dist 7 | /examples 8 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2017 Matt Mukerjee 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # studio_one_session_parser 2 | Parsing library for Presonus Studio One sessions 3 | -------------------------------------------------------------------------------- /__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mukerjee/studio_one_session_parser/388e44ce8ea6c0c73ba79453ec1b3ead82646f08/__init__.py -------------------------------------------------------------------------------- /parsers/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mukerjee/studio_one_session_parser/388e44ce8ea6c0c73ba79453ec1b3ead82646f08/parsers/__init__.py -------------------------------------------------------------------------------- /parsers/audio_mixer.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/python 2 | 3 | import sys 4 | from lxml import etree 5 | from song_parser import Parser 6 | 7 | 8 | class AudioMixer(Parser): 9 | def __init__(self, fn): 10 | super(AudioMixer, self).__init__(fn) 11 | 12 | self.channels = {} # maps channel uid to XML 13 | 14 | for child in self.tree.xpath( 15 | "Attributes[@x:id='channels']/ChannelGroup", 16 | namespaces=self.ns): 17 | self.channels.update(self.parse_channels(child)) 18 | 19 | def parse_channels(self, root): 20 | return {child.xpath("UID")[0].get("uid"): child for child in root} 21 | 22 | def get_inserts(self, channelID): 23 | i = self.channels[channelID].xpath( 24 | "Attributes[@x:id='Inserts']/Attributes/String[@x:id='presetPath']", 25 | namespaces=self.ns) 26 | return ["/" + a.get("text") for a in i] 27 | 28 | def get_name(self, channelID): 29 | return self.channels[channelID].get("label") 30 | 31 | def get_type(self, channelID): 32 | return self.channels[channelID].tag \ 33 | if channelID in self.channels else 'None' 34 | 35 | def get_destination(self, channelID): 36 | d = self.channels[channelID].xpath( 37 | "Connection[@x:id='destination']", namespaces=self.ns) 38 | return d[0].get("objectID").split("/")[0] if len(d) else None 39 | 40 | def get_vca(self, channelID): 41 | vca = self.channels[channelID].xpath( 42 | "Attributes[@x:id='VCATarget']/Connection[@x:id='vcaTarget']", 43 | namespaces=self.ns) 44 | return vca[0].get("objectID").split("/")[0] if len(vca) else None 45 | 46 | def get_sends(self, channelID): 47 | sends = self.channels[channelID].xpath( 48 | "Attributes[@x:id='Sends']/*/Connection[@x:id='destination']", 49 | namespaces=self.ns) 50 | return [s.get("objectID").split("/")[0] for s in sends] 51 | 52 | def add_channel(self, channel): 53 | cgname = channel.tag.split("Channel")[0] 54 | cg = self.tree.xpath("*/ChannelGroup[@name='%s']" % cgname) 55 | if not len(cg): 56 | cg = etree.fromstring('' 57 | % cgname) 58 | self.tree.xpath("Attributes[@name='Channels']")[0].append(cg) 59 | cg = [cg] 60 | cg[0].append(channel) 61 | uid = channel.xpath("UID")[0].get("uid") 62 | self.channels[uid] = channel 63 | 64 | if __name__ == "__main__": 65 | am = AudioMixer(sys.argv[1]) 66 | uid = am.channels.keys()[0] 67 | print uid 68 | print am.get_name(uid) 69 | print am.get_destination(uid) 70 | print am.get_inserts(uid) 71 | -------------------------------------------------------------------------------- /parsers/audio_synth_folder.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/python 2 | 3 | import sys 4 | from song_parser import Parser 5 | 6 | 7 | class AudioSynthFolder(Parser): 8 | def __init__(self, fn): 9 | super(AudioSynthFolder, self).__init__(fn) 10 | 11 | self.synths = {} # maps synth deviceData UID to XML Synth Attributes 12 | 13 | for synth in self.tree.xpath("Attributes[@name]"): 14 | a = synth.xpath( 15 | "Attributes[@x:id='deviceData']/UID", namespaces=self.ns)[0] 16 | self.synths[a.get("uid")] = synth 17 | 18 | def get_name(self, uid): 19 | return self.synths[uid].get("name") 20 | 21 | def get_synth_name(self, uid): 22 | a = self.synths[uid].xpath( 23 | "Attributes[@x:id='deviceData']", namespaces=self.ns)[0] 24 | return a.get("name") 25 | 26 | def get_synth_preset(self, uid): 27 | a = self.synths[uid].xpath( 28 | "String[@x:id='presetPath']", namespaces=self.ns)[0] 29 | return "/" + a.get("text") 30 | 31 | def add_synth(self, synth): 32 | self.tree.append(synth) 33 | a = synth.xpath( 34 | "Attributes[@x:id='deviceData']/UID", namespaces=self.ns)[0] 35 | self.synths[a.get("uid")] = synth 36 | 37 | if __name__ == "__main__": 38 | asf = AudioSynthFolder(sys.argv[1]) 39 | print asf.synths 40 | tid = asf.synths.keys()[0] 41 | print asf.get_synth_name(tid) 42 | print asf.get_name(tid) 43 | print asf.get_synth_preset(tid) 44 | -------------------------------------------------------------------------------- /parsers/media_pool.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/python 2 | 3 | import sys 4 | from lxml import etree 5 | from song_parser import Parser 6 | 7 | 8 | class MediaPool(Parser): 9 | def __init__(self, fn): 10 | super(MediaPool, self).__init__(fn) 11 | 12 | self.clips = {} # maps mediaID to XML 13 | self.packages = [] # list of XML package Association 14 | self.doc_path = None # XML documentPath 15 | 16 | for c in self.tree.xpath( 17 | "Attributes/MediaFolder[@name='Music']/MusicClip | " + 18 | "Attributes/MediaFolder[@name='Audio']/AudioClip | " + 19 | "Attributes/MediaFolder[@name='Sound']/ExternalClip | " + 20 | "Attributes/MediaFolder[@name='AudioEffects']/*"): 21 | self.clips[c.get("mediaID")] = c 22 | 23 | for p in self.tree.xpath( 24 | "Attributes[@x:id='packageInfos']/Association", 25 | namespaces=self.ns): 26 | self.packages.append(p) 27 | 28 | self.doc_path = self.tree.xpath( 29 | "Attributes[@x:id='documentPath']", namespaces=self.ns)[0] 30 | 31 | def get_file(self, mediaID): 32 | return self.clips[mediaID].xpath("Url")[0].get("url").split("//")[1] 33 | 34 | def get_clip_effect_files(self, mediaID): 35 | cs = self.clips[mediaID].xpath("List/AudioEffectClipItem/Url") 36 | return [c.get("url").split("media://")[1] for c in cs] 37 | 38 | def get_doc_path(self): 39 | return self.doc_path.get("url").split("//")[1] 40 | 41 | def add_clip(self, clip): 42 | d = {"MusicClip": "Music", "AudioClip": "Audio", 43 | "ExternalClip": "Sound", 'AudioEffectClip': 'AudioEffects'} 44 | name = d[clip.tag] 45 | mf = self.tree.xpath("Attributes/MediaFolder[@name='%s']" % name) 46 | if not len(mf): 47 | mf = etree.fromstring('' % name) 48 | self.tree.xpath("Attributes[@x:id='rootFolder']", 49 | namespaces=self.ns)[0].append(mf) 50 | mf = [mf] 51 | mf[0].append(clip) 52 | self.clips[clip.get("mediaID")] = clip 53 | 54 | def add_package(self, package): 55 | self.packages[0].getparent().append(package) 56 | self.packages.append(package) 57 | 58 | def set_doc_path(self, doc_path): 59 | self.swap(self.doc_path, doc_path) 60 | self.doc_path = doc_path 61 | 62 | if __name__ == "__main__": 63 | mp = MediaPool(sys.argv[1]) 64 | print mp.get_file(mp.clips.keys()[0]) 65 | print mp.get_doc_path() 66 | -------------------------------------------------------------------------------- /parsers/mixer_console.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/python 2 | 3 | import sys 4 | import copy 5 | from lxml import etree 6 | from song_parser import Parser 7 | 8 | 9 | class MixerConsole(Parser): 10 | def __init__(self, fn): 11 | super(MixerConsole, self).__init__(fn) 12 | 13 | self.channel_settings = {} # maps (correct) UIDs to XML Section 14 | self.channel_banks = {} # maps bank id to XML ChannelShowHidPresets 15 | self.channels_in_bank = {} # maps UIDs to XML UID 16 | self.max = 0 17 | 18 | for c in self.tree.xpath( 19 | "Attributes[@x:id='channelSettings']/*", namespaces=self.ns): 20 | self.max = max(self.max, int(c[0].get("order"))) 21 | self.channel_settings[self.fix_uid(c.get("path"))] = c 22 | 23 | for c in self.tree.xpath( 24 | "Attributes[@x:id='channelBanks']/*", namespaces=self.ns): 25 | self.channel_banks[c.get("{x}id")] = c 26 | for t in c.xpath("List[@x:id='visible']/UID", namespaces=self.ns): 27 | self.channels_in_bank[t.get("uid")] = t 28 | 29 | def get_visible_in_bank(self, bank): 30 | return [v.get("uid") for v in self.channel_banks[bank].xpath( 31 | "List[@x:id='visible']/UID", namespaces=self.ns)] 32 | 33 | def add_channel_setting(self, channel_setting): 34 | self.tree.xpath( 35 | "Attributes[@x:id='channelSettings']", 36 | namespaces=self.ns)[0].append(channel_setting) 37 | a = channel_setting.xpath("Attributes")[0] 38 | a.set("order", str(int(a.get("order")) + self.max)) 39 | uid = self.fix_uid(channel_setting.get("path")) 40 | self.channel_settings[uid] = channel_setting 41 | 42 | def add_channel_to_banks(self, channel): 43 | for bank in self.channel_banks.values(): 44 | ch = copy.deepcopy(channel) 45 | v = bank.xpath("List[@x:id='visible']", namespaces=self.ns) 46 | if not len(v): 47 | v = etree.SubElement(bank, "List") 48 | v.set("{x}id", "visible") 49 | v = [v] 50 | v[0].append(ch) 51 | 52 | if __name__ == "__main__": 53 | mc = MixerConsole(sys.argv[1]) 54 | for c in mc.get_visible_in_bank("ScreenBank"): 55 | print c in mc.channel_settings 56 | -------------------------------------------------------------------------------- /parsers/music_track_device.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/python 2 | 3 | import sys 4 | from lxml import etree 5 | from song_parser import Parser 6 | 7 | 8 | class MusicTrackDevice(Parser): 9 | def __init__(self, fn): 10 | super(MusicTrackDevice, self).__init__(fn) 11 | 12 | self.channels = {} # maps UID to XML MusicTrackChannel 13 | 14 | for c in self.tree.xpath( 15 | "Attributes[@name='Channels']/ChannelGroup/*"): 16 | self.channels[c.xpath("UID")[0].get("uid")] = c 17 | 18 | def get_instrument_out(self, uid): 19 | c = self.channels[uid].xpath( 20 | "Connection[@x:id='instrumentOut']", namespaces=self.ns) 21 | return c[0].get("objectID").split('/')[0] if len(c) else None 22 | 23 | def get_destination(self, uid): 24 | c = self.channels[uid].xpath( 25 | "Connection[@x:id='destination']", namespaces=self.ns) 26 | return c[0].get("objectID").split('/')[0] if len(c) else None 27 | 28 | def add_channel(self, channel): 29 | cg = self.tree.xpath("Attributes/ChannelGroup[@name='MusicTrack']") 30 | if not len(cg): 31 | cg = etree.fromstring( 32 | "") 33 | self.tree.xpath("Attributes")[0].append(cg) 34 | cg = [cg] 35 | cg[0].append(channel) 36 | 37 | self.channels[channel.xpath("UID")[0].get("uid")] = channel 38 | 39 | 40 | if __name__ == "__main__": 41 | mtd = MusicTrackDevice(sys.argv[1]) 42 | print mtd.get_instrument_out(mtd.channels.keys()[0]) 43 | print mtd.get_destination(mtd.channels.keys()[0]) 44 | -------------------------------------------------------------------------------- /parsers/song.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/python 2 | 3 | import sys 4 | import hashlib 5 | from lxml import etree 6 | from collections import OrderedDict 7 | from song_parser import Parser 8 | 9 | 10 | class Song(Parser): 11 | def __init__(self, fn): 12 | super(Song, self).__init__(fn) 13 | 14 | tc = self.tree.xpath( 15 | "Attributes[@x:id='Root']/Attributes[@x:id='timeContext']", 16 | namespaces=self.ns)[0] 17 | 18 | self.tempo_map = tc.xpath("TempoMap")[0] 19 | self.time_sig_map = tc.xpath("TimeSignatureMap")[0] 20 | self.time_zone_map = tc.xpath("TimeZoneMap")[0] 21 | 22 | t = self.tree.xpath("Attributes[@x:id='Root']/List[@x:id='Tracks']", 23 | namespaces=self.ns)[0] 24 | 25 | self.marker_track = t.xpath("MarkerTrack")[0] 26 | self.arranger_track = t.xpath("ArrangerTrack")[0] 27 | 28 | self.tracks = OrderedDict() # maps trackID to XML MediaTrack 29 | self.track_names = OrderedDict() # maps track names to trackID 30 | 31 | for c in t.xpath("MediaTrack | AutomationTrack | FolderTrack"): 32 | if c.tag == "MediaTrack" and c.get("trackID") is None: 33 | h = self.fix_uid(hashlib.md5(etree.tostring(c)).hexdigest()) 34 | c.set("trackID", h) 35 | self.tracks[c.get("trackID")] = c 36 | self.track_names[c.get("name")] = c.get("trackID") 37 | 38 | def get_track_type(self, trackID): 39 | t = self.tracks[trackID] 40 | if t.tag == "MediaTrack": 41 | return t.get("mediaType") 42 | elif t.tag == "AutomationTrack": 43 | return "Automation" 44 | elif t.tag == "FolderTrack": 45 | return "Folder" 46 | else: 47 | return None 48 | 49 | def get_track_name(self, trackID): 50 | return self.tracks[trackID].get("name") 51 | 52 | def get_folder(self, trackID): 53 | return self.tracks[trackID].get("parentFolder") 54 | 55 | def get_channel_id(self, trackID): 56 | c = self.tracks[trackID].xpath( 57 | "UID[@x:id='channelID']", namespaces=self.ns) 58 | return c[0].get("uid") if len(c) else None 59 | 60 | def get_clip_ids(self, trackID): 61 | c = self.tracks[trackID].xpath( 62 | "*/MusicPart | */AudioEvent | */*/*/MusicPart | */*/*/AudioEvent") 63 | return [mp.get("clipID") for mp in c] 64 | 65 | def get_clip_effect_ids(self, trackID): 66 | c = self.tracks[trackID].xpath( 67 | "*/AudioEvent/Attributes[@x:id='effects'] | " + 68 | "*/*/*/AudioEvent/Attributes[@x:id='effects']", namespaces=self.ns) 69 | return [ce.get("clipID") for ce in c] 70 | 71 | def get_automation(self, trackID): 72 | a = self.tracks[trackID].xpath( 73 | "Attributes[@x:id='AutomationRegionList']/AutomationRegion/Url", 74 | namespaces=self.ns) 75 | return [b.get("url").split("media://")[1] for b in a] 76 | 77 | def set_tempo_map(self, tm): 78 | self.swap(self.tempo_map, tm) 79 | self.tempo_map = tm 80 | 81 | def set_time_sig_map(self, tsm): 82 | self.swap(self.time_sig_map, tsm) 83 | self.time_sig_map = tsm 84 | 85 | def set_time_zone_map(self, tzm): 86 | self.swap(self.time_zone_map, tzm) 87 | self.time_zone_map = tzm 88 | 89 | def set_marker_track(self, mt): 90 | self.swap(self.marker_track, mt) 91 | self.marker_track = mt 92 | 93 | def set_arranger_track(self, at): 94 | self.swap(self.arranger_track, at) 95 | self.arranger_track = at 96 | 97 | def add_track(self, track): 98 | self.tree.xpath( 99 | "Attributes[@x:id='Root']/List[@x:id='Tracks']", 100 | namespaces=self.ns)[0].append(track) 101 | self.tracks[track.get("trackID")] = track 102 | self.track_names[track.get("name")] = track.get("trackID") 103 | 104 | 105 | if __name__ == "__main__": 106 | s = Song(sys.argv[1]) 107 | name = s.track_names.keys()[1] 108 | print name 109 | tid = s.track_names[name] 110 | print s.get_channel_id(tid) 111 | print s.get_clip_ids(tid) 112 | -------------------------------------------------------------------------------- /parsers/song_parser.py: -------------------------------------------------------------------------------- 1 | from lxml import etree as ElementTree 2 | 3 | 4 | class Parser(object): 5 | def __init__(self, fn): 6 | self.fn = fn 7 | xml = open(fn).read() 8 | if 'xmlns' not in xml: 9 | xml = xml.split('\n') 10 | xml[1] = xml[1].split('>')[0] + ' xmlns:x="x">' 11 | xml = '\n'.join(xml) 12 | parser = ElementTree.XMLParser(remove_blank_text=True) 13 | self.tree = ElementTree.fromstring(xml, parser) 14 | self.ns = {'x': 'x'} 15 | 16 | def write(self): 17 | open(self.fn, 'w').write(ElementTree.tostring(self.tree, 18 | pretty_print=True)) 19 | parser = ElementTree.XMLParser(remove_blank_text=True) 20 | xml = open(self.fn).read() 21 | self.tree = ElementTree.fromstring(xml, parser) 22 | open(self.fn, 'w').write(ElementTree.tostring(self.tree, 23 | pretty_print=True)) 24 | 25 | def swap(self, old, new): 26 | p = old.getparent() 27 | p.replace(old, new) 28 | 29 | def fix_uid(self, uid): 30 | return '{%s-%s-%s-%s-%s}' % (uid[:8], uid[8:12], 31 | uid[12:16], uid[16:20], uid[20:]) 32 | 33 | -------------------------------------------------------------------------------- /song_model.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/python 2 | 3 | import os 4 | import zipfile 5 | import shutil 6 | import tempfile 7 | 8 | from parsers.audio_mixer import AudioMixer 9 | from parsers.audio_synth_folder import AudioSynthFolder 10 | from parsers.media_pool import MediaPool 11 | from parsers.mixer_console import MixerConsole 12 | from parsers.music_track_device import MusicTrackDevice 13 | from parsers.song import Song 14 | 15 | DONT_OVERWRITE = True 16 | USE_TEMP = True 17 | 18 | 19 | class SongModel(object): 20 | def __init__(self, fn): 21 | self.is_clean = True 22 | self.fn = fn 23 | self.prefix = None 24 | self.extract() 25 | if DONT_OVERWRITE: 26 | self.fn = os.path.splitext(self.fn)[0] + '-new.song' 27 | self.song = Song(self.prefix + '/Song/song.xml') 28 | self.musictrackdevice = MusicTrackDevice( 29 | self.prefix + '/Devices/musictrackdevice.xml') 30 | self.audiosynthfolder = AudioSynthFolder( 31 | self.prefix + '/Devices/audiosynthfolder.xml') 32 | self.mediapool = MediaPool(self.prefix + '/Song/mediapool.xml') 33 | self.mixerconsole = MixerConsole( 34 | self.prefix + '/Devices/mixerconsole.xml') 35 | self.audiomixer = AudioMixer(self.prefix + '/Devices/audiomixer.xml') 36 | 37 | def extract(self): 38 | f = zipfile.ZipFile(self.fn, 'r') 39 | if USE_TEMP: 40 | self.prefix = tempfile.mkdtemp() 41 | else: 42 | self.prefix = os.path.splitext(self.fn)[0] 43 | f.extractall(self.prefix) 44 | f.close() 45 | self.is_clean = False 46 | 47 | def compress(self): 48 | f = zipfile.ZipFile(self.fn, 'w', zipfile.ZIP_DEFLATED) 49 | old_dir = os.getcwd() 50 | os.chdir(self.prefix) 51 | for root, dirs, files in os.walk('./'): 52 | for file in files: 53 | f.write(os.path.join(root, file)) 54 | f.close() 55 | os.chdir(old_dir) 56 | 57 | def delete_temp(self): 58 | if not self.is_clean: 59 | shutil.rmtree(self.prefix) 60 | self.is_clean = True 61 | 62 | def clean(self): 63 | if not self.is_clean: 64 | self.delete_temp() 65 | self.is_clean = True 66 | 67 | def write(self): 68 | self.song.write() 69 | self.musictrackdevice.write() 70 | self.audiosynthfolder.write() 71 | self.mediapool.write() 72 | self.mixerconsole.write() 73 | self.audiomixer.write() 74 | self.compress() 75 | self.delete_temp() 76 | 77 | --------------------------------------------------------------------------------