Fix name for /disable_aftertouch.
[calfbox.git] / py / cbox.py
blobbaf9d92a2816d75a26e86150bfaea45e94baf48a
1 from _cbox import *
2 import struct
3 import traceback
5 type_wrapper_debug = False
7 ###############################################################################
8 # Ugly internals. Please skip this section for your own sanity.
9 ###############################################################################
11 class GetUUID:
12 """An object that calls a C layer command, receives a /uuid callback from it
13 and stores the passed UUID in its uuid attribute.
15 Example use: GetUUID('/command', arg1, arg2...).uuid
16 """
17 def __init__(self, cmd, *cmd_args):
18 def callback(cmd, fb, args):
19 if cmd == "/uuid" and len(args) == 1:
20 self.uuid = args[0]
21 else:
22 raise ValueException("Unexpected callback: %s" % cmd)
23 self.callback = callback
24 self.uuid = None
25 do_cmd(cmd, self, list(cmd_args))
26 def __call__(self, *args):
27 self.callback(*args)
29 class GetThings:
30 """A generic callback object that receives various forms of information from
31 C layer and converts then into object's Python attributes.
33 This is an obsolete interface, to be replaced by GetUUID or metaclass
34 based type-safe autoconverter. However, there are still some cases that
35 aren't (yet) handled by either.
36 """
37 @staticmethod
38 def by_uuid(uuid, cmd, anames, args):
39 return GetThings(Document.uuid_cmd(uuid, cmd), anames, args)
40 def __init__(self, cmd, anames, args):
41 for i in anames:
42 if i.startswith("*"):
43 setattr(self, i[1:], [])
44 elif i.startswith("%"):
45 setattr(self, i[1:], {})
46 else:
47 setattr(self, i, None)
48 anames = set(anames)
49 self.seq = []
50 def update_callback(cmd, fb, args):
51 self.seq.append((cmd, fb, args))
52 cmd = cmd[1:]
53 if cmd in anames:
54 if len(args) == 1:
55 setattr(self, cmd, args[0])
56 else:
57 setattr(self, cmd, args)
58 elif "*" + cmd in anames:
59 if len(args) == 1:
60 getattr(self, cmd).append(args[0])
61 else:
62 getattr(self, cmd).append(args)
63 elif "%" + cmd in anames:
64 if len(args) == 2:
65 getattr(self, cmd)[args[0]] = args[1]
66 else:
67 getattr(self, cmd)[args[0]] = args[1:]
68 elif len(args) == 1:
69 setattr(self, cmd, args[0])
70 do_cmd(cmd, update_callback, args)
71 def __str__(self):
72 return str(self.seq)
74 class PropertyDecorator(object):
75 """Abstract property decorator."""
76 def __init__(self, base):
77 self.base = base
78 def get_base(self):
79 return self.base
80 def map_cmd(self, cmd):
81 return cmd
83 class AltPropName(PropertyDecorator):
84 """Command-name-changing property decorator. Binds a property to the
85 specified /path, different from the default one, which based on property name,
86 with -s and -es suffix removed for lists and dicts."""
87 def __init__(self, alt_name, base):
88 PropertyDecorator.__init__(self, base)
89 self.alt_name = alt_name
90 def map_cmd(self, cmd):
91 return self.alt_name
92 def execute(self, property, proptype, klass):
93 pass
95 class SettableProperty(PropertyDecorator):
96 """Decorator that creates a setter method for the property."""
97 def execute(self, property, proptype, klass):
98 if type(proptype) is dict:
99 setattr(klass, 'set_' + property, lambda self, key, value: self.cmd('/' + property, None, key, proptype(value)))
100 elif type(proptype) is bool:
101 setattr(klass, 'set_' + property, lambda self, value: self.cmd('/' + property, None, 1 if value else 0))
102 else:
103 setattr(klass, 'set_' + property, lambda self, value: self.cmd('/' + property, None, proptype(value)))
105 def new_get_things(obj, cmd, settermap, args):
106 """Call C command with arguments 'args', populating a return object obj
107 using settermap to interpret callback commands and initialise the return
108 object."""
109 def update_callback(cmd2, fb, args2):
110 try:
111 if cmd2 in settermap:
112 settermap[cmd2](obj, args2)
113 except Exception as error:
114 traceback.print_exc()
115 raise
116 # Set initial values for the properties (None or empty dict/list)
117 for setterobj in settermap.values():
118 setattr(obj, setterobj.property, setterobj.init_value())
119 # Call command and apply callback commands via setters to the object
120 do_cmd(cmd, update_callback, args)
121 return obj
123 def _handle_object_wrapping(t):
124 if type(t) is CboxObjMetaclass:
125 return lambda uuid: Document.map_uuid_and_check(uuid, t)
126 return t
127 def _make_args_to_type_lambda(t):
128 t = _handle_object_wrapping(t)
129 return lambda args: t(*args)
130 def _make_args_to_tuple_of_types_lambda(ts):
131 ts = list(map(_handle_object_wrapping, ts))
132 return lambda args: tuple([ts[i](args[i]) for i in range(max(len(ts), len(args)))])
134 class SetterWithConversion:
135 """A setter object class that sets a specific property to a typed value or a tuple of typed value."""
136 def __init__(self, property, extractor):
137 self.property = property
138 self.extractor = extractor
139 def init_value(self):
140 return None
141 def __call__(self, obj, args):
142 # print ("Setting attr %s on object %s" % (self.property, obj))
143 setattr(obj, self.property, self.extractor(args))
145 class ListAdderWithConversion:
146 """A setter object class that adds a tuple filled with type-converted arguments of the
147 callback to a list. E.g. ListAdderWithConversion('foo', (int, int))(obj, [1,2])
148 adds a tuple: (int(1), int(2)) to the list obj.foo"""
150 def __init__(self, property, extractor):
151 self.property = property
152 self.extractor = extractor
153 def init_value(self):
154 return []
155 def __call__(self, obj, args):
156 getattr(obj, self.property).append(self.extractor(args))
158 class DictAdderWithConversion:
159 """A setter object class that adds a tuple filled with type-converted
160 arguments of the callback to a dictionary under a key passed as first argument
161 i.e. DictAdderWithConversion('foo', str, (int, int))(obj, ['bar',1,2]) adds
162 a tuple: (int(1), int(2)) under key 'bar' to obj.foo"""
164 def __init__(self, property, keytype, valueextractor):
165 self.property = property
166 self.keytype = keytype
167 self.valueextractor = valueextractor
168 def init_value(self):
169 return {}
170 def __call__(self, obj, args):
171 getattr(obj, self.property)[self.keytype(args[0])] = self.valueextractor(args[1:])
173 def _create_unmarshaller(name, fields_to_unmarshall, base_type):
174 status_fields = []
175 all_decorators = {}
176 prop_types = {}
177 settermap = {}
178 if type_wrapper_debug:
179 print ("Wrapping type: %s" % name)
180 print ("-----")
181 for prop in dir(fields_to_unmarshall):
182 if prop.startswith("__"):
183 continue
184 value = getattr(fields_to_unmarshall, prop)
185 decorators = []
186 propcmd = '/' + prop
187 if type(value) is list or type(value) is dict:
188 if propcmd.endswith('s'):
189 if propcmd.endswith('es'):
190 propcmd = propcmd[:-2]
191 else:
192 propcmd = propcmd[:-1]
193 while isinstance(value, PropertyDecorator):
194 decorators.append(value)
195 propcmd = value.map_cmd(propcmd)
196 value = value.get_base()
198 def _make_decoder(t):
199 if type(t) is tuple:
200 return _make_args_to_tuple_of_types_lambda(t)
201 else:
202 return _make_args_to_type_lambda(t)
204 if type(value) in [type, CboxObjMetaclass, tuple]:
205 if type_wrapper_debug:
206 print ("%s is type %s" % (prop, repr(value)))
207 status_fields.append(prop)
208 settermap[propcmd] = SetterWithConversion(prop, _make_decoder(value))
209 elif type(value) is dict:
210 assert(len(value) == 1)
211 value = list(value.items())[0]
212 if type_wrapper_debug:
213 print ("%s is type: %s -> %s" % (prop, repr(value[0]), repr(value[1])))
214 settermap[propcmd] = DictAdderWithConversion(prop, value[0], _make_decoder(value[1]))
215 elif type(value) is list:
216 assert(len(value) == 1)
217 if type_wrapper_debug:
218 print ("%s is array of %s" % (prop, repr(value)))
219 settermap[propcmd] = ListAdderWithConversion(prop, _make_decoder(value[0]))
220 else:
221 raise ValueError("Don't know what to do with %s property '%s' of type %s" % (name, prop, repr(value)))
222 all_decorators[prop] = decorators
223 prop_types[prop] = value
224 fields_to_unmarshall.__str__ = lambda self: (str(name) + ":" + " ".join(["%s=%s" % (v.property, repr(getattr(self, v.property))) for v in settermap.values()]))
225 if type_wrapper_debug:
226 print ("")
227 cmdwrapper = lambda cmd: (lambda self: new_get_things(base_type(), self.path + cmd, settermap, []))
228 def exec_cmds(o):
229 for propname, decorators in all_decorators.items():
230 for decorator in decorators:
231 decorator.execute(propname, prop_types[propname], o)
232 return exec_cmds, cmdwrapper
234 class CboxObjMetaclass(type):
235 """Metaclass that creates Python wrapper classes for various C-side objects.
236 This class is responsible for automatically marshalling and type-checking/converting
237 fields of Status inner class on status() calls."""
238 def __new__(cls, name, bases, namespace, **kwds):
239 status_class = namespace['Status']
240 classfinaliser, cmdwrapper = _create_unmarshaller(name, status_class, status_class)
241 result = type.__new__(cls, name, bases, namespace, **kwds)
242 classfinaliser(result)
243 result.status = cmdwrapper('/status')
244 return result
246 class NonDocObj(object, metaclass = CboxObjMetaclass):
247 """Root class for all wrapper classes that wrap objects that don't have
248 their own identity/UUID.
249 This covers various singletons and inner objects (e.g. engine in instrument)."""
250 class Status:
251 pass
252 def __init__(self, path):
253 self.path = path
255 def cmd(self, cmd, fb = None, *args):
256 do_cmd(self.path + cmd, fb, list(args))
258 def cmd_makeobj(self, cmd, *args):
259 return Document.map_uuid(GetUUID(self.path + cmd, *args).uuid)
261 def get_things(self, cmd, fields, *args):
262 return GetThings(self.path + cmd, fields, list(args))
264 def make_path(self, path):
265 return self.path + path
267 class DocObj(NonDocObj):
268 """Root class for all wrapper classes that wrap first-class document objects."""
269 class Status:
270 pass
271 def __init__(self, uuid):
272 NonDocObj.__init__(self, Document.uuid_cmd(uuid, ''))
273 self.uuid = uuid
275 def delete(self):
276 self.cmd("/delete")
278 class VarPath:
279 def __init__(self, path, args = []):
280 self.path = path
281 self.args = args
282 def plus(self, subpath, *args):
283 return VarPath(self.path if subpath is None else self.path + "/" + subpath, self.args + list(args))
284 def set(self, *values):
285 do_cmd(self.path, None, self.args + list(values))
287 ###############################################################################
288 # And those are the proper user-accessible objects.
289 ###############################################################################
291 class Config:
292 """INI file manipulation class."""
293 @staticmethod
294 def sections(prefix = ""):
295 """Return a list of configuration sections."""
296 return [CfgSection(name) for name in GetThings('/config/sections', ['*section'], [str(prefix)]).section]
298 @staticmethod
299 def keys(section, prefix = ""):
300 """Return a list of configuration keys in a section, with optional prefix filtering."""
301 return GetThings('/config/keys', ['*key'], [str(section), str(prefix)]).key
303 @staticmethod
304 def get(section, key):
305 """Return a string value of a given key."""
306 return GetThings('/config/get', ['value'], [str(section), str(key)]).value
308 @staticmethod
309 def set(section, key, value):
310 """Set a string value for a given key."""
311 do_cmd('/config/set', None, [str(section), str(key), str(value)])
313 @staticmethod
314 def delete(section, key):
315 """Delete a given key."""
316 do_cmd('/config/delete', None, [str(section), str(key)])
318 @staticmethod
319 def save(filename = None):
320 """Save config, either into current INI file or some other file."""
321 if filename is None:
322 do_cmd('/config/save', None, [])
323 else:
324 do_cmd('/config/save', None, [str(filename)])
326 @staticmethod
327 def add_section(section, content):
328 """Populate a config section based on a string with key=value lists.
329 This is a toy/debug function, it doesn't handle any edge cases."""
330 for line in content.splitlines():
331 line = line.strip()
332 if line == '' or line.startswith('#'):
333 continue
334 try:
335 key, value = line.split("=", 2)
336 except ValueError as err:
337 raise ValueError("Cannot parse config line '%s'" % line)
338 Config.set(section, key.strip(), value.strip())
340 class Transport:
341 @staticmethod
342 def seek_ppqn(ppqn):
343 do_cmd('/master/seek_ppqn', None, [int(ppqn)])
344 @staticmethod
345 def seek_samples(samples):
346 do_cmd('/master/seek_samples', None, [int(samples)])
347 @staticmethod
348 def set_tempo(tempo):
349 do_cmd('/master/set_tempo', None, [float(tempo)])
350 @staticmethod
351 def set_timesig(nom, denom):
352 do_cmd('/master/set_timesig', None, [int(nom), int(denom)])
353 @staticmethod
354 def play():
355 do_cmd('/master/play', None, [])
356 @staticmethod
357 def stop():
358 do_cmd('/master/stop', None, [])
359 @staticmethod
360 def panic():
361 do_cmd('/master/panic', None, [])
362 @staticmethod
363 def status():
364 return GetThings("/master/status", ['pos', 'pos_ppqn', 'tempo', 'timesig', 'sample_rate', 'playing'], [])
365 @staticmethod
366 def tell():
367 return GetThings("/master/tell", ['pos', 'pos_ppqn', 'playing'], [])
368 @staticmethod
369 def ppqn_to_samples(pos_ppqn):
370 return GetThings("/master/ppqn_to_samples", ['value'], [pos_ppqn]).value
371 @staticmethod
372 def samples_to_ppqn(pos_samples):
373 return GetThings("/master/samples_to_ppqn", ['value'], [pos_samples]).value
375 # Currently responsible for both JACK and USB I/O - not all functionality is
376 # supported by both.
377 class JackIO:
378 AUDIO_TYPE = "32 bit float mono audio"
379 MIDI_TYPE = "8 bit raw midi"
380 PORT_IS_SINK = 0x1
381 PORT_IS_SOURCE = 0x2
382 PORT_IS_PHYSICAL = 0x4
383 PORT_CAN_MONITOR = 0x8
384 PORT_IS_TERMINAL = 0x10
385 @staticmethod
386 def status():
387 # Some of these only make sense for
388 return GetThings("/io/status", ['client_type', 'client_name', 'audio_inputs', 'audio_outputs', 'buffer_size', '*midi_output', '*midi_input', 'sample_rate', 'output_resolution'], [])
389 @staticmethod
390 def create_midi_input(name, autoconnect_spec = None):
391 uuid = GetUUID("/io/create_midi_input", name).uuid
392 if autoconnect_spec is not None and autoconnect_spec != '':
393 JackIO.autoconnect(uuid, autoconnect_spec)
394 return uuid
395 @staticmethod
396 def create_midi_output(name, autoconnect_spec = None):
397 uuid = GetUUID("/io/create_midi_output", name).uuid
398 if autoconnect_spec is not None and autoconnect_spec != '':
399 JackIO.autoconnect(uuid, autoconnect_spec)
400 return uuid
401 @staticmethod
402 def autoconnect_midi_output(uuid, autoconnect_spec = None):
403 if autoconnect_spec is not None:
404 do_cmd("/io/autoconnect", None, [uuid, autoconnect_spec])
405 else:
406 do_cmd("/io/autoconnect", None, [uuid, ''])
407 autoconnect_midi_input = autoconnect_midi_output
408 @staticmethod
409 def rename_midi_output(uuid, new_name):
410 do_cmd("/io/rename_midi_port", None, [uuid, new_name])
411 rename_midi_input = rename_midi_output
412 @staticmethod
413 def disconnect_midi_output(uuid):
414 do_cmd("/io/disconnect_midi_port", None, [uuid])
415 disconnect_midi_input = disconnect_midi_output
416 @staticmethod
417 def disconnect_midi_output(uuid):
418 do_cmd("/io/disconnect_midi_output", None, [uuid])
419 @staticmethod
420 def delete_midi_input(uuid):
421 do_cmd("/io/delete_midi_input", None, [uuid])
422 @staticmethod
423 def delete_midi_output(uuid):
424 do_cmd("/io/delete_midi_output", None, [uuid])
425 @staticmethod
426 def port_connect(pfrom, pto):
427 do_cmd("/io/port_connect", None, [pfrom, pto])
428 @staticmethod
429 def port_disconnect(pfrom, pto):
430 do_cmd("/io/port_disconnect", None, [pfrom, pto])
431 @staticmethod
432 def get_ports(name_mask = ".*", type_mask = ".*", flag_mask = 0):
433 return GetThings("/io/get_ports", ['*port'], [name_mask, type_mask, int(flag_mask)]).port
435 def call_on_idle(callback = None):
436 do_cmd("/on_idle", callback, [])
438 def get_new_events():
439 return GetThings("/on_idle", ['seq'], []).seq
441 def send_midi_event(*data, output = None):
442 do_cmd('/send_event_to', None, [output if output is not None else ''] + list(data))
444 class CfgSection:
445 def __init__(self, name):
446 self.name = name
448 def __getitem__(self, key):
449 return Config.get(self.name, key)
451 def __setitem__(self, key, value):
452 Config.set(self.name, key, value)
454 def __delitem__(self, key):
455 Config.delete(self.name, key)
457 def keys(self, prefix = ""):
458 return Config.keys(self.name, prefix)
461 class Pattern:
462 @staticmethod
463 def get_pattern():
464 pat_data = GetThings("/get_pattern", ['pattern'], []).pattern
465 if pat_data is not None:
466 pat_blob, length = pat_data
467 pat_data = []
468 ofs = 0
469 while ofs < len(pat_blob):
470 data = list(struct.unpack_from("iBBbb", pat_blob, ofs))
471 data[1:2] = []
472 pat_data.append(tuple(data))
473 ofs += 8
474 return pat_data, length
475 return None
477 @staticmethod
478 def serialize_event(time, *data):
479 if len(data) >= 1 and len(data) <= 3:
480 return struct.pack("iBBbb"[0:2 + len(data)], int(time), len(data), *[int(v) for v in data])
481 raise ValueError("Invalid length of an event (%d)" % len(data))
483 class Document:
484 """Document singleton."""
485 classmap = {}
486 objmap = {}
487 @staticmethod
488 def dump():
489 """Print all objects in the documents to stdout. Only used for debugging."""
490 do_cmd("/doc/dump", None, [])
491 @staticmethod
492 def uuid_cmd(uuid, cmd):
493 """Internal: execute a given request on an object with specific UUID."""
494 return "/doc/uuid/%s%s" % (uuid, cmd)
495 @staticmethod
496 def get_uuid(path):
497 """Internal: retrieve an UUID of an object that has specified path."""
498 return GetUUID('%s/get_uuid' % path).uuid
499 @staticmethod
500 def get_obj_class(uuid):
501 """Internal: retrieve an internal class type of an object that has specified path."""
502 return GetThings(Document.uuid_cmd(uuid, "/get_class_name"), ["class_name"], []).class_name
503 @staticmethod
504 def get_song():
505 """Retrieve the current song object of a given document. Each document can
506 only have one current song."""
507 return Document.map_uuid(Document.get_uuid("/song"))
508 @staticmethod
509 def get_scene():
510 """Retrieve the current scene object of a given document. Each document can
511 only have one current scene."""
512 return Document.map_uuid(Document.get_uuid("/scene"))
513 @staticmethod
514 def get_rt():
515 """Retrieve the RT singleton. RT is an object used to communicate between
516 realtime and user thread, and is currently also used to access the audio
517 engine."""
518 return Document.map_uuid(Document.get_uuid("/rt"))
519 @staticmethod
520 def new_scene(srate, bufsize):
521 """Create a new scene object. This new scene object cannot be used for
522 audio playback - that's only allowed for main document scene."""
523 return Document.map_uuid(GetUUID('/new_scene', int(srate), int(bufsize)).uuid)
524 @staticmethod
525 def map_uuid(uuid):
526 """Create or retrieve a Python-side accessor proxy for a C-side object."""
527 if uuid in Document.objmap:
528 return Document.objmap[uuid]
529 try:
530 oclass = Document.get_obj_class(uuid)
531 except Exception as e:
532 print ("Note: Cannot get class for " + uuid)
533 Document.dump()
534 raise
535 o = Document.classmap[oclass](uuid)
536 Document.objmap[uuid] = o
537 if hasattr(o, 'init_object'):
538 o.init_object()
539 return o
540 @staticmethod
541 def map_uuid_and_check(uuid, t):
542 o = Document.map_uuid(uuid)
543 if not isinstance(o, t):
544 raise TypeError("UUID %s is of type %s, expected %s" % (uuid, o.__class__, t))
545 return o
547 class DocPattern(DocObj):
548 class Status:
549 event_count = int
550 loop_end = int
551 name = str
552 def __init__(self, uuid):
553 DocObj.__init__(self, uuid)
554 def set_name(self, name):
555 self.cmd("/name", None, name)
556 Document.classmap['cbox_midi_pattern'] = DocPattern
558 class ClipItem:
559 def __init__(self, pos, offset, length, pattern, clip):
560 self.pos = pos
561 self.offset = offset
562 self.length = length
563 self.pattern = Document.map_uuid(pattern)
564 self.clip = Document.map_uuid(clip)
565 def __str__(self):
566 return "pos=%d offset=%d length=%d pattern=%s clip=%s" % (self.pos, self.offset, self.length, self.pattern.uuid, self.clip.uuid)
567 def __eq__(self, other):
568 return str(self) == str(other)
570 class DocTrackClip(DocObj):
571 class Status:
572 pos = int
573 offset = int
574 length = int
575 pattern = DocPattern
576 def __init__(self, uuid):
577 DocObj.__init__(self, uuid)
578 Document.classmap['cbox_track_item'] = DocTrackClip
580 class DocTrack(DocObj):
581 class Status:
582 clips = [ClipItem]
583 name = SettableProperty(str)
584 external_output = SettableProperty(int)
585 def add_clip(self, pos, offset, length, pattern):
586 return self.cmd_makeobj("/add_clip", int(pos), int(offset), int(length), pattern.uuid)
587 Document.classmap['cbox_track'] = DocTrack
589 class TrackItem:
590 def __init__(self, name, count, track):
591 self.name = name
592 self.count = count
593 self.track = Document.map_uuid(track)
595 class PatternItem:
596 def __init__(self, name, length, pattern):
597 self.name = name
598 self.length = length
599 self.pattern = Document.map_uuid(pattern)
601 class MtiItem:
602 def __init__(self, pos, tempo, timesig_nom, timesig_denom):
603 self.pos = pos
604 self.tempo = tempo
605 self.timesig_nom = timesig_nom
606 self.timesig_denom = timesig_denom
607 def __eq__(self, o):
608 return self.pos == o.pos and self.tempo == o.tempo and self.timesig_nom == o.timesig_nom and self.timesig_denom == o.timesig_denom
610 class DocSongStatus:
611 tracks = None
612 patterns = None
614 class DocSong(DocObj):
615 class Status:
616 tracks = [TrackItem]
617 patterns = [PatternItem]
618 mtis = [MtiItem]
619 loop_start = int
620 loop_end = int
621 def clear(self):
622 return self.cmd("/clear", None)
623 def set_loop(self, ls, le):
624 return self.cmd("/set_loop", None, int(ls), int(le))
625 def set_mti(self, pos, tempo = None, timesig_nom = None, timesig_denom = None):
626 self.cmd("/set_mti", None, int(pos), float(tempo) if tempo is not None else -1.0, int(timesig_nom) if timesig_nom is not None else -1, int(timesig_denom) if timesig_denom else -1)
627 def add_track(self):
628 return self.cmd_makeobj("/add_track")
629 def load_drum_pattern(self, name):
630 return self.cmd_makeobj("/load_pattern", name, 1)
631 def load_drum_track(self, name):
632 return self.cmd_makeobj("/load_track", name, 1)
633 def pattern_from_blob(self, blob, length):
634 return self.cmd_makeobj("/load_blob", bytearray(blob), int(length))
635 def loop_single_pattern(self, loader):
636 self.clear()
637 track = self.add_track()
638 pat = loader()
639 length = pat.status().loop_end
640 track.add_clip(0, 0, length, pat)
641 self.set_loop(0, length)
642 self.update_playback()
643 def update_playback(self):
644 # XXXKF Maybe make it a song-level API instead of global
645 do_cmd("/update_playback", None, [])
646 Document.classmap['cbox_song'] = DocSong
648 class DocInstrument(DocObj):
649 class Status:
650 name = str
651 outputs = int
652 aux_offset = int
653 engine = str
654 def init_object(self):
655 engine = self.status().engine
656 if engine in engine_classes:
657 self.engine = engine_classes[engine]("/doc/uuid/" + self.uuid + "/engine")
658 Document.classmap['cbox_instrument'] = DocInstrument
660 class DocLayer(DocObj):
661 class Status:
662 name = str
663 instrument_name = str
664 instrument = AltPropName('/instrument_uuid', DocInstrument)
665 enable = SettableProperty(bool)
666 low_note = SettableProperty(int)
667 high_note = SettableProperty(int)
668 fixed_note = SettableProperty(int)
669 in_channel = SettableProperty(int)
670 out_channel = SettableProperty(int)
671 disable_aftertouch = SettableProperty(bool)
672 invert_sustain = SettableProperty(bool)
673 consume = SettableProperty(bool)
674 ignore_scene_transpose = SettableProperty(bool)
675 ignore_program_changes = SettableProperty(bool)
676 transpose = SettableProperty(int)
677 def get_instrument(self):
678 return self.status().instrument
679 Document.classmap['cbox_layer'] = DocLayer
681 class SamplerEngine(NonDocObj):
682 class Status(object):
683 """Maximum number of voices playing at the same time."""
684 polyphony = int
685 """Current number of voices playing."""
686 active_voices = int
687 """GM volume (14-bit) per MIDI channel."""
688 volume = {int:int}
689 """GM pan (14-bit) per MIDI channel."""
690 pan = {int:int}
691 """Current number of voices playing per MIDI channel."""
692 channel_voices = AltPropName('/channel_voices', {int:int})
693 """MIDI channel -> (program number, program name)"""
694 patches = {int:(int, str)}
696 def load_patch_from_cfg(self, patch_no, cfg_section, display_name):
697 """Load a sampler program from an 'spgm:' config section."""
698 return self.cmd_makeobj("/load_patch", int(patch_no), cfg_section, display_name)
700 def load_patch_from_string(self, patch_no, sample_dir, sfz_data, display_name):
701 """Load a sampler program from a string, using given filesystem path for sample directory."""
702 return self.cmd_makeobj("/load_patch_from_string", int(patch_no), sample_dir, sfz_data, display_name)
704 def load_patch_from_file(self, patch_no, sfz_name, display_name):
705 """Load a sampler program from a filesystem file."""
706 return self.cmd_makeobj("/load_patch_from_file", int(patch_no), sfz_name, display_name)
708 def set_patch(self, channel, patch_no):
709 """Select patch identified by patch_no in a specified MIDI channel."""
710 self.cmd("/set_patch", None, int(channel), int(patch_no))
711 def get_unused_program(self):
712 """Returns first program number that has no program associated with it."""
713 return self.get_things("/get_unused_program", ['program_no']).program_no
714 def set_polyphony(self, polyphony):
715 """Set a maximum number of voices that can be played at a given time."""
716 self.cmd("/polyphony", None, int(polyphony))
717 def get_patches(self):
718 """Return a map of program identifiers to program objects."""
719 return self.get_things("/patches", ['%patch']).patch
721 class FluidsynthEngine(NonDocObj):
722 class Status:
723 polyphony = int
724 soundfont = str
725 patch = {int: int}
726 def load_soundfont(self, filename):
727 return self.cmd_makeobj("/load_soundfont", filename)
728 def set_patch(self, channel, patch_no):
729 self.cmd("/set_patch", None, int(channel), int(patch_no))
730 def set_polyphony(self, polyphony):
731 self.cmd("/polyphony", None, int(polyphony))
732 def get_patches(self):
733 return self.get_things("/patches", ['%patch']).patch
735 class StreamPlayerEngine(NonDocObj):
736 class Status:
737 filename = str
738 pos = int
739 length = int
740 playing = int
741 def play(self):
742 self.cmd('/play')
743 def stop(self):
744 self.cmd('/stop')
745 def seek(self, place):
746 self.cmd('/seek', None, int(place))
747 def load(self, filename, loop_start = -1):
748 self.cmd('/load', None, filename, int(loop_start))
749 def unload(self, filename, loop_start = -1):
750 self.cmd('/unload')
752 class TonewheelOrganEngine(NonDocObj):
753 class Status:
754 upper_drawbar = SettableProperty({int: int})
755 lower_drawbar = SettableProperty({int: int})
756 pedal_drawbar = SettableProperty({int: int})
757 upper_vibrato = SettableProperty(bool)
758 lower_vibrato = SettableProperty(bool)
759 vibrato_mode = SettableProperty(int)
760 vibrato_chorus = SettableProperty(int)
761 percussion_enable = SettableProperty(bool)
762 percussion_3rd = SettableProperty(bool)
764 engine_classes = {
765 'sampler' : SamplerEngine,
766 'fluidsynth' : FluidsynthEngine,
767 'stream_player' : StreamPlayerEngine,
768 'tonewheel_organ' : TonewheelOrganEngine,
771 class DocAuxBus(DocObj):
772 class Status:
773 name = str
774 def get_slot_engine(self):
775 return self.cmd_makeobj("/slot/engine/get_uuid")
776 def get_slot_status(self):
777 return self.get_things("/slot/status", ["insert_preset", "insert_engine"])
778 Document.classmap['cbox_aux_bus'] = DocAuxBus
780 class DocScene(DocObj):
781 class Status:
782 name = str
783 title = str
784 transpose = int
785 layers = [DocLayer]
786 instruments = {str: (str, DocInstrument)}
787 auxes = {str: (str, str, DocAuxBus)}
788 def clear(self):
789 self.cmd("/clear", None)
790 def load(self, name):
791 self.cmd("/load", None, name)
792 def load_aux(self, aux):
793 return self.cmd_makeobj("/load_aux", aux)
794 def delete_aux(self, aux):
795 return self.cmd("/delete_aux", None, aux)
796 def delete_layer(self, pos):
797 self.cmd("/delete_layer", None, int(1 + pos))
798 def move_layer(self, old_pos, new_pos):
799 self.cmd("/move_layer", None, int(old_pos + 1), int(new_pos + 1))
801 def add_layer(self, aux, pos = None):
802 if pos is None:
803 return self.cmd_makeobj("/add_layer", 0, aux)
804 else:
805 # Note: The positions in high-level API are zero-based.
806 return self.cmd_makeobj("/add_layer", int(1 + pos), aux)
807 def add_instrument_layer(self, name, pos = None):
808 if pos is None:
809 return self.cmd_makeobj("/add_instrument_layer", 0, name)
810 else:
811 return self.cmd_makeobj("/add_instrument_layer", int(1 + pos), name)
812 def add_new_instrument_layer(self, name, engine, pos = None):
813 if pos is None:
814 return self.cmd_makeobj("/add_new_instrument_layer", 0, name, engine)
815 else:
816 return self.cmd_makeobj("/add_new_instrument_layer", int(1 + pos), name, engine)
817 Document.classmap['cbox_scene'] = DocScene
819 class DocRt(DocObj):
820 class Status:
821 audio_channels = (int, int)
822 state = (int, str)
823 Document.classmap['cbox_rt'] = DocRt
825 class DocModule(DocObj):
826 class Status:
827 pass
828 Document.classmap['cbox_module'] = DocModule
830 class SamplerProgram(DocObj):
831 class Status:
832 name = str
833 sample_dir = str
834 program_no = int
835 in_use = int
836 def get_regions(self):
837 return map(Document.map_uuid, self.get_things("/regions", ['*region']).region)
838 def get_groups(self):
839 g = self.get_things("/groups", ['*group', 'default_group'])
840 return [Document.map_uuid(g.default_group)] + list(map(Document.map_uuid, g.group))
841 def get_control_inits(self):
842 return self.get_things("/control_inits", ['*control_init']).control_init
843 def new_group(self):
844 return self.cmd_makeobj("/new_group")
845 def add_control_init(self, controller, value):
846 return self.cmd("/add_control_init", None, controller, value)
847 # which = -1 -> remove all controllers with that number from the list
848 def delete_control_init(self, controller, which = 0):
849 return self.cmd("/delete_control_init", None, controller, which)
850 Document.classmap['sampler_program'] = SamplerProgram
852 class SamplerLayer(DocObj):
853 class Status:
854 parent_program = SamplerProgram
855 parent_group = DocObj
856 def get_children(self):
857 return map(Document.map_uuid, self.get_things("/get_children", ['*region']).region)
858 def as_string(self):
859 return self.get_things("/as_string", ['value']).value
860 def as_string_full(self):
861 return self.get_things("/as_string_full", ['value']).value
862 def set_param(self, key, value):
863 self.cmd("/set_param", None, key, str(value))
864 def new_region(self):
865 return self.cmd_makeobj("/new_region")
866 Document.classmap['sampler_layer'] = SamplerLayer