-
Notifications
You must be signed in to change notification settings - Fork 0
/
exporter.py
397 lines (305 loc) · 14.2 KB
/
exporter.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
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
import logging
from xml.dom.minidom import getDOMImplementation
from lisp.core.plugin import PluginNotLoadedError
from lisp.plugins import get_plugin
from .exporters import find_exporters
from .util import CUEID_MARKUP_PREFIX, CUEID_MARKUP_SUFFIX, ExportKeys, ScsAudioDevice, ScsDeviceType, SCS_XML_INDENT
logger = logging.getLogger(__name__) # pylint: disable=invalid-name
class ScsExporter:
def __init__(self, app):
self._app = app
self._impl = getDOMImplementation()
self._dom = None
self._prod_id = None
# Find exporters (but don't init them)
self._exporters = {}
for name, exporter in find_exporters():
cuetype = exporter.lisp_cuetype
if cuetype in self._exporters:
logger.warn(f"Already registered an exporter for cue type {cuetype}")
continue
logger.debug(f"Registering exporter for {cuetype}: {name}.")
self._exporters[cuetype] = exporter
@property
def dom(self):
return self._dom
def _split_cue_name(self, lisp_cue):
"""
See comment for CUEID_MARKUP_PREFIX in util.py
"""
cue_id = lisp_cue.index + 1
cue_name = lisp_cue.name
if cue_name.startswith(CUEID_MARKUP_PREFIX):
cue_name_split = cue_name[len(CUEID_MARKUP_PREFIX):].split(CUEID_MARKUP_SUFFIX, 1)
if len(cue_name_split) == 2:
cue_name = cue_name_split[1]
if cue_name_split[0].isalnum():
cue_id = cue_name_split[0]
return cue_id, cue_name
def export(self, prod_id, cues):
# Get used cue types
cuetypes = {cue.__class__.__name__ for cue in self._app.layout.cues()}
for cuetype in cuetypes:
# Check we have an exporter for each cue type
if cuetype not in self._exporters:
logger.warning(f"No registered exporter for Cues of type {cuetype}")
continue
# And if we do, initialise an instance of it, if needed
if isinstance(self._exporters[cuetype], type):
self._exporters[cuetype] = self._exporters[cuetype]()
self._prod_id = prod_id
self._dom = self._impl.createDocument(None, "Production", None)
devices = {}
for devtype in ScsDeviceType:
devices[devtype] = set()
document = self._dom.documentElement
for lisp_cue in cues:
cue_type = lisp_cue.__class__.__name__
if cue_type not in self._exporters:
# A warning has already been given if no appropriate exporter is present
continue
exported = self._exporters[cue_type].export_cue(self, lisp_cue)
if not exported:
logger.warning(f"{cue_type} '{lisp_cue.name}' not exported.")
continue
for scs_cue in exported[ExportKeys.Cues]:
document.appendChild(scs_cue)
if ExportKeys.Device in exported:
device_type, device_details = exported[ExportKeys.Device]
devices[device_type].add(device_details)
document.insertBefore(self.build_production_head(devices), document.firstChild)
return self._dom
def create_text_element(self, element_name, content):
if isinstance(content, bool):
content = int(content)
if isinstance(content, int) or isinstance(content, float):
content = str(content)
element = self._dom.createElement(element_name)
element.appendChild(
self._dom.createTextNode(content))
return element
def build_audio_definitions(self, devices):
"""
Devices for playing audio from Audio files
.. note: At least one audio device must be defined, even if no audio cues exist.
.. note: Mappings to actual physical devices are done locally on a machine.
"""
if not len(devices):
devices.add(ScsAudioDevice(name='Placeholder', channels=2))
definitions = []
idx = 0
for device in devices:
# User-definable identifier
definitions.append(self.create_text_element(f"PRLogicalDev{idx}", device.name))
# Device Channel Count
# @todo: Get channel count of device
definitions.append(self.create_text_element(f"PRNumChans{idx}", device.channels))
# Automatically include device in new audio cues (optional)
# Default: False
# @todo: True only if this is the default ALSA device, or if using Jack
if not idx:
definitions.append(self.create_text_element(f"PRAutoIncludeDev{idx}", True))
# The Audio Device to use when Previewing an Audio File
# We use the first device defined for this.
if not idx:
definitions.append(self.create_text_element("PreviewDevice", device.name))
idx += 1
return definitions
def build_control_rx_definitions(self):
"""
SCS supports being remotely controlled by either MIDI or RS232.
LiSP supports MIDI or OSC.
SCS supports up to two Devices (and there should be a definition
for each); LiSP only one. Hoever, each SCS MIDI "device" is
locked to a single MIDI channel, whilst LiSP allows full use of
all channels on a device.
"""
try:
midi = get_plugin("Midi")
controller = get_plugin("Controller")
except PluginNotLoadedError:
return []
if not midi.is_loaded() or not controller.is_loaded():
return []
midi_controls = controller.Config.get('protocols.midi', None)
if not midi_controls:
return []
from lisp.plugins.controller.common import LayoutAction
from lisp.plugins.midi.midi_utils import midi_str_to_dict
# Group the commands together by MIDI Channel
control_definitions = [[] for _ in range(16)]
for control in midi_controls:
control_msg = midi_str_to_dict(control[0])
control_definitions[control_msg['channel']].append([control_msg] + list(control[1:]))
# Sort by how much each Channel is used
control_definitions.sort(key=len, reverse=True)
action_dict = {
LayoutAction.Go.name: "Go",
LayoutAction.StopAll.name: "StopAll",
LayoutAction.InterruptAll.name: "StopAll",
LayoutAction.StandbyBack.name: "GoBack",
LayoutAction.StandbyForward.name: "GoNext",
}
devices = []
# Transfer only the two most frequently used Channels
for definition_set in control_definitions[:2]:
if not definition_set:
continue
device = self._dom.createElement("PRCCDevice")
# Device Type:
# MIDIIn | RS232In
device.appendChild(
self.create_text_element("PRCCDevType", "MIDIIn"))
# MIDIIn Control Method:
# Custom | ETC AB | ETC CD | MMC | MSC | ON | Palladium | PC127 | PC128
#
# This appears to be used as a "template" for initial
# creation of assignments in the SCS UI, rather than
# limiting what can and what can't be sent from this device.
device.appendChild(
self.create_text_element("PRCCMidiCtrlMethod", "Custom"))
# MIDI Channel:
# Req. unless PRCCMidiCtrlMethod is MMC | MSC
# integer; 1 -> 16
device.appendChild(
self.create_text_element("PRCCMidiChannel", definition_set[0][0]['channel'] + 1))
# Set the commands used by this device
for command_definition in definition_set:
command_dict = command_definition[0]
command_action = command_definition[1]
if command_action not in action_dict:
logger.warn(f"Action {command_action} not aliased.")
continue
if command_dict['type'] == 'program_change':
cmd = 0xC
cc = command_dict['program']
vv = None
elif command_dict['type'] in ['note_on', 'note_off']:
cmd = 0x8 if command_dict['type'] == 'note_on' else 0x9
cc = command_dict['note']
vv = command_dict['velocity']
elif command_dict['type'] == 'control_change':
cmd = 0xB
cc = command_dict['control']
vv = command_dict['value']
else:
logger.warn(f"Non-configured command: {command_dict}")
continue
midi_command = self._dom.createElement("PRCCMidiCommand")
midi_command.appendChild(
self.create_text_element("PRCCMidiCmdType", action_dict[command_action]))
midi_command.appendChild(
self.create_text_element("PRCCMidiCmd", cmd))
midi_command.appendChild(
self.create_text_element("PRCCMidiCC", cc))
if vv is not None:
midi_command.appendChild(
self.create_text_element("PRCCMidiVV", vv))
device.appendChild(midi_command)
devices.append(device)
return devices
def build_control_tx_definitions(self, devices):
"""
SCS supports being sending either MIDI or RS232. LiSP supports
MIDI or OSC.
LiSP (currently) supports only one MIDI output; the quantity SCS
supports depends on the version:
* "Demo" : 1
* "Lite" : 0
* "Standard" : 0
* "Professional": 4
* "Pro Plus" : 8
* "Platinum" : 16
"""
try:
midi = get_plugin("Midi")
if not midi.is_loaded():
return []
except PluginNotLoadedError:
return []
scs_devices = []
for spec in devices:
prcs_device = self._dom.createElement("PRCSDevice")
# User-definable identifier for the device
prcs_device.appendChild(
self.create_text_element("PRCSLogicalDev", spec.name))
# Device Type
# MIDIOut | RS232Out
prcs_device.appendChild(
self.create_text_element("PRCSDevType", "MIDIOut"))
scs_devices.append(prcs_device)
return scs_devices
def build_generic_cue(self, lisp_cue):
"""Creates a new SCS cue. Must have at least one SubCue.
Encapsulating Node: Cue
Sub Nodes:
Required:
CueID string e.g. "Q1"
Description string Used in the SCS UI as a Cue Name
Optional:
PageNo string
WhenReqd string
DefDes bool int Indicates whether <Description/> is default
Enabled bool int
ActivationMethod enum "auto" | <??>
Seem optional, but may be required if ActivationMethod == "auto":
AutoActivateCue string CueId
AutoActivatePosn enum "start" | <??>
AutoActivateTime integer <milliseconds>
"""
scs_cue = self._dom.createElement("Cue")
cue_id, cue_name = self._split_cue_name(lisp_cue)
scs_cue.appendChild(self.create_text_element("CueID", cue_id))
scs_cue.appendChild(self.create_text_element("Description", cue_name))
if lisp_cue.description:
scs_cue.appendChild(
self.create_text_element("WhenReqd", lisp_cue.description.replace("\n\n", "\n")))
return scs_cue
def build_generic_subcue(self, lisp_cue, scs_cuetype):
"""Creates a new SubCue. Each Cue must have at least one SubCue.
Encapsulating Node: Sub
Sub Nodes:
Required:
SubType enum "M" (Midi)
| "F" (Audio)
| "S" (Fade Out And/Or Stop)
| "L" (Volume Level Change)
| "K" (Lighting Control)
Optional:
SubDescription string Used in the SCS UI as SubCue Name
DefSubDes bool int Indicates whether <SubDescription/> is default
RelStartMode enum "ae_prev_sub" | "as_cue" | "as_prev_sub"
RelStartTime integer <milliseconds>
"""
_, cue_name = self._split_cue_name(lisp_cue)
scs_subcue = self._dom.createElement("Sub")
scs_subcue.appendChild(self.create_text_element("SubType", scs_cuetype))
scs_subcue.appendChild(self.create_text_element("SubDescription", cue_name))
return scs_subcue
def build_production_head(self, devices):
head = self._dom.createElement("Head")
# Name of the Production
head.appendChild(self.create_text_element("Title", self._app.session.name()))
# Unique ID of the SCS Production
if self._prod_id:
head.appendChild(self.create_text_element("ProdId", self._prod_id))
for element in self.build_audio_definitions(devices[ScsDeviceType.Audio]):
head.appendChild(element)
for element in self.build_videoaudio_definitions(devices[ScsDeviceType.VideoAudio]):
head.appendChild(element)
for element in self.build_control_tx_definitions(devices[ScsDeviceType.Midi]):
head.appendChild(element)
for element in self.build_control_rx_definitions():
head.appendChild(element)
return head
def build_videoaudio_definitions(self, devices):
"""
Devices for playing audio from Video files
"""
definitions = []
idx = 0
for device in devices:
# User-definable identifier
definitions.append(self.create_text_element(f"PRVidAudLogicalDev{idx}", device.name))
idx += 1
return definitions