-
Notifications
You must be signed in to change notification settings - Fork 6
/
Copy pathardour2fxp.py
executable file
·186 lines (152 loc) · 6.19 KB
/
ardour2fxp.py
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
#!/usr/bin/env python
# -*- coding: utf-8 -*-
#
# ardour2fxp.py
#
"""Convert one or more Ardour VST presets XML file to VST2 FXP preset files."""
import argparse
import os
import sys
from base64 import b64decode
from collections import namedtuple
from os.path import exists, isdir, join
from struct import calcsize, pack
from xml.etree import ElementTree as ET
FXP_HEADER_FMT = '>4si4s4i28s'
FXP_PREAMBEL_SIZE = calcsize('>4si')
FXP_HEADER_SIZE = calcsize(FXP_HEADER_FMT)
FXP_FORMAT_VERSION = 1
CHUNK_MAGIC = b'CcnK'
FX_MAGIC_PARAMS = b'FxCk'
FX_MAGIC_CHUNK = b'FPCh'
FX_DEFAULT_VERSION = 1
PRESET_BASE_FIELDS = (
'plugin_id',
'plugin_version',
'hash',
'label',
'num_params',
)
ChunkPreset = namedtuple('ChunkPreset', PRESET_BASE_FIELDS + ('chunk',))
Preset = namedtuple('Preset', PRESET_BASE_FIELDS + ('params',))
def label2fn(label):
"""Replace characters in label unsuitable for filenames with underscore."""
return label.strip().replace(' ', '_')
def parse_ardourpresets(root):
"""Parse ardour VST presets XML document.
Returns list of Preset or ChunkPreset instances.
"""
if root.tag != 'VSTPresets':
raise ValueError("Root node must be 'VSTPresets'.")
presets = []
for preset in root:
if preset.tag not in ('Preset', 'ChunkPreset'):
print("Invalid preset type: {}".format(preset.tag))
continue
try:
type, plugin_id, hash = preset.attrib['uri'].split(':', 2)
plugin_id = int(plugin_id)
version = preset.attrib.get('version')
num_params = preset.attrib.get('numParams')
label = preset.attrib['label']
if version is not None:
version = int(version)
if num_params is not None:
num_params = int(num_params)
if type != "VST":
raise ValueError
except (KeyError, ValueError):
print("Invalid preset format: {}".format(preset.attrib))
continue
if preset.tag == 'Preset':
params = {int(param.attrib['index']): param.attrib['value']
for param in preset}
params = [float(value) for _, value in sorted(params.items())]
presets.append(Preset(plugin_id, version, hash, label, num_params,
params))
elif preset.tag == 'ChunkPreset':
presets.append(ChunkPreset(plugin_id, version, hash, label,
num_params, b64decode(preset.text)))
return presets
def main(args=None):
argparser = argparse.ArgumentParser()
argparser.add_argument('-v', '--fx-version', type=int,
help="VST plugin version number")
argparser.add_argument('-f', '--force', action="store_true",
help="Overwrite existing destination file(s)")
argparser.add_argument('-o', '--output-dir',
help="Ardour presets output directory")
argparser.add_argument('infiles', nargs='*', metavar='XML',
help="Ardour VST presets XML (input) file(s)")
args = argparser.parse_args(args)
output_dir = args.output_dir or os.getcwd()
if not args.infiles:
argparser.print_help()
return 2
for infile in args.infiles:
try:
root_node = ET.parse(infile).getroot()
presets = parse_ardourpresets(root_node)
except Exception as exc:
return "Error reading Ardour preset file '{}': {}".format(
infile, exc)
if not presets:
return "No valid presets found in input file(s)."
for preset in presets:
plugin_id = pack('>I', preset.plugin_id).decode('ascii')
dstdir = join(output_dir, plugin_id)
if not isdir(dstdir):
os.makedirs(dstdir)
fxp_fn = join(dstdir, label2fn(preset.label)) + '.fxp'
if exists(fxp_fn) and not args.force:
print("FXP output file '{}' already exists. Skipping".format(
fxp_fn))
continue
with open(fxp_fn, 'wb') as fp:
if args.fx_version is not None:
fx_version = args.fx_version
elif preset.plugin_version is not None:
fx_version = preset.plugin_version
else:
fx_version = FX_DEFAULT_VERSION
if isinstance(preset, Preset):
if preset.num_params is None:
num_params = len(preset.params)
else:
num_params = preset.num_params
params_fmt = '>{:d}f'.format(num_params)
size = (FXP_HEADER_SIZE - FXP_PREAMBEL_SIZE +
calcsize(params_fmt))
fx_magic = FX_MAGIC_PARAMS
elif isinstance(preset, ChunkPreset):
if preset.num_params is None:
num_params = int(len(preset.chunk) / 4)
else:
num_params = preset.num_params
chunk_len = len(preset.chunk)
chunk_size = pack('>i', chunk_len)
size = (FXP_HEADER_SIZE - FXP_PREAMBEL_SIZE +
len(chunk_size) + chunk_len)
fx_magic = FX_MAGIC_CHUNK
else:
raise TypeError("Wrong preset type: {!r}".format(preset))
header = pack(
FXP_HEADER_FMT,
CHUNK_MAGIC,
size,
fx_magic,
FXP_FORMAT_VERSION,
preset.plugin_id,
fx_version,
num_params,
preset.label.encode('latin1', errors='replace')
)
fp.write(header)
if isinstance(preset, Preset):
data = pack(params_fmt, *preset.params)
fp.write(data)
elif isinstance(preset, ChunkPreset):
fp.write(chunk_size)
fp.write(preset.chunk)
if __name__ == '__main__':
sys.exit(main() or 0)