├── .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 |
--------------------------------------------------------------------------------