Fix broken bounds check.
[calfbox.git] / py / cbox.py
blob35265ec715ee539b59a2e8312509d68f4a531122
1 from _cbox import *
2 from io import BytesIO
3 import struct
4 import traceback
6 type_wrapper_debug = False
8 ###############################################################################
9 # Ugly internals. Please skip this section for your own sanity.
10 ###############################################################################
12 class GetUUID:
13 """An object that calls a C layer command, receives a /uuid callback from it
14 and stores the passed UUID in its uuid attribute.
16 Example use: GetUUID('/command', arg1, arg2...).uuid
17 """
18 def __init__(self, cmd, *cmd_args):
19 def callback(cmd, fb, args):
20 if cmd == "/uuid" and len(args) == 1:
21 self.uuid = args[0]
22 else:
23 raise ValueException("Unexpected callback: %s" % cmd)
24 self.callback = callback
25 self.uuid = None
26 do_cmd(cmd, self, list(cmd_args))
27 def __call__(self, *args):
28 self.callback(*args)
30 class GetThings:
31 """A generic callback object that receives various forms of information from
32 C layer and converts then into object's Python attributes.
34 This is an obsolete interface, to be replaced by GetUUID or metaclass
35 based type-safe autoconverter. However, there are still some cases that
36 aren't (yet) handled by either.
37 """
38 @staticmethod
39 def by_uuid(uuid, cmd, anames, args):
40 return GetThings(Document.uuid_cmd(uuid, cmd), anames, args)
41 def __init__(self, cmd, anames, args):
42 for i in anames:
43 if i.startswith("*"):
44 setattr(self, i[1:], [])
45 elif i.startswith("%"):
46 setattr(self, i[1:], {})
47 else:
48 setattr(self, i, None)
49 anames = set(anames)
50 self.seq = []
51 def update_callback(cmd, fb, args):
52 self.seq.append((cmd, fb, args))
53 cmd = cmd[1:]
54 if cmd in anames:
55 if len(args) == 1:
56 setattr(self, cmd, args[0])
57 else:
58 setattr(self, cmd, args)
59 elif "*" + cmd in anames:
60 if len(args) == 1:
61 getattr(self, cmd).append(args[0])
62 else:
63 getattr(self, cmd).append(args)
64 elif "%" + cmd in anames:
65 if len(args) == 2:
66 getattr(self, cmd)[args[0]] = args[1]
67 else:
68 getattr(self, cmd)[args[0]] = args[1:]
69 elif len(args) == 1:
70 setattr(self, cmd, args[0])
71 do_cmd(cmd, update_callback, args)
72 def __str__(self):
73 return str(self.seq)
75 class PropertyDecorator(object):
76 """Abstract property decorator."""
77 def __init__(self, base):
78 self.base = base
79 def get_base(self):
80 return self.base
81 def map_cmd(self, cmd):
82 return cmd
84 class AltPropName(PropertyDecorator):
85 """Command-name-changing property decorator. Binds a property to the
86 specified /path, different from the default one, which based on property name,
87 with -s and -es suffix removed for lists and dicts."""
88 def __init__(self, alt_name, base):
89 PropertyDecorator.__init__(self, base)
90 self.alt_name = alt_name
91 def map_cmd(self, cmd):
92 return self.alt_name
93 def execute(self, property, proptype, klass):
94 pass
96 class SettableProperty(PropertyDecorator):
97 """Decorator that creates a setter method for the property."""
98 def execute(self, property, proptype, klass):
99 if type(proptype) is dict:
100 setattr(klass, 'set_' + property, lambda self, key, value: self.cmd('/' + property, None, key, value))
101 elif type(proptype) is bool:
102 setattr(klass, 'set_' + property, lambda self, value: self.cmd('/' + property, None, 1 if value else 0))
103 else:
104 setattr(klass, 'set_' + property, lambda self, value: self.cmd('/' + property, None, proptype(value)))
106 def new_get_things(obj, cmd, settermap, args):
107 """Call C command with arguments 'args', populating a return object obj
108 using settermap to interpret callback commands and initialise the return
109 object."""
110 def update_callback(cmd2, fb, args2):
111 try:
112 if cmd2 in settermap:
113 settermap[cmd2](obj, args2)
114 elif cmd2 != '/uuid': # Ignore UUID as it's usually safe to do so
115 print ("Unexpected command: %s" % cmd2)
116 except Exception as error:
117 traceback.print_exc()
118 raise
119 # Set initial values for the properties (None or empty dict/list)
120 for setterobj in settermap.values():
121 setattr(obj, setterobj.property, setterobj.init_value())
122 # Call command and apply callback commands via setters to the object
123 do_cmd(cmd, update_callback, args)
124 return obj
126 def _error_arg_mismatch(required, passed):
127 raise ValueError("Types required: %s, values passed: %s" % (repr(required), repr(passed)))
128 def _handle_object_wrapping(t):
129 if type(t) is CboxObjMetaclass:
130 return lambda uuid: Document.map_uuid_and_check(uuid, t)
131 return t
132 def _make_args_to_type_lambda(t):
133 t = _handle_object_wrapping(t)
134 return lambda args: t(*args)
135 def _make_args_to_tuple_of_types_lambda(ts):
136 ts = list(map(_handle_object_wrapping, ts))
137 return lambda args: tuple([ts[i](args[i]) for i in range(max(len(ts), len(args)))]) if len(ts) == len(args) else _error_arg_mismatch(ts, args)
138 def _make_args_decoder(t):
139 if type(t) is tuple:
140 return _make_args_to_tuple_of_types_lambda(t)
141 else:
142 return _make_args_to_type_lambda(t)
144 def get_thing(cmd, fieldcmd, datatype, *args):
145 if type(datatype) is list:
146 assert (len(datatype) == 1)
147 decoder = _make_args_decoder(datatype[0])
148 value = []
149 def adder(data):
150 value.append(decoder(data))
151 elif type(datatype) is dict:
152 assert (len(datatype) == 1)
153 key_type, value_type = list(datatype.items())[0]
154 key_decoder = _make_args_decoder(key_type)
155 value_decoder = _make_args_decoder(value_type)
156 value = {}
157 def adder(data):
158 value[key_decoder([data[0]])] = value_decoder(data[1:])
159 else:
160 decoder = _make_args_decoder(datatype)
161 def adder(data):
162 nonlocal value
163 value = decoder(data)
164 value = None
165 def callback(cmd2, fb, args2):
166 if cmd2 == fieldcmd:
167 adder(args2)
168 else:
169 print ("Unexpected command %s" % cmd2)
170 do_cmd(cmd, callback, list(args))
171 return value
173 class SetterWithConversion:
174 """A setter object class that sets a specific property to a typed value or a tuple of typed value."""
175 def __init__(self, property, extractor):
176 self.property = property
177 self.extractor = extractor
178 def init_value(self):
179 return None
180 def __call__(self, obj, args):
181 # print ("Setting attr %s on object %s" % (self.property, obj))
182 setattr(obj, self.property, self.extractor(args))
184 class ListAdderWithConversion:
185 """A setter object class that adds a tuple filled with type-converted arguments of the
186 callback to a list. E.g. ListAdderWithConversion('foo', (int, int))(obj, [1,2])
187 adds a tuple: (int(1), int(2)) to the list obj.foo"""
189 def __init__(self, property, extractor):
190 self.property = property
191 self.extractor = extractor
192 def init_value(self):
193 return []
194 def __call__(self, obj, args):
195 getattr(obj, self.property).append(self.extractor(args))
197 class DictAdderWithConversion:
198 """A setter object class that adds a tuple filled with type-converted
199 arguments of the callback to a dictionary under a key passed as first argument
200 i.e. DictAdderWithConversion('foo', str, (int, int))(obj, ['bar',1,2]) adds
201 a tuple: (int(1), int(2)) under key 'bar' to obj.foo"""
203 def __init__(self, property, keytype, valueextractor):
204 self.property = property
205 self.keytype = keytype
206 self.valueextractor = valueextractor
207 def init_value(self):
208 return {}
209 def __call__(self, obj, args):
210 getattr(obj, self.property)[self.keytype(args[0])] = self.valueextractor(args[1:])
212 def _type_properties(base_type):
213 return {prop: getattr(base_type, prop) for prop in dir(base_type) if not prop.startswith("__")}
215 def _create_setter(prop, t):
216 if type(t) in [type, CboxObjMetaclass, tuple]:
217 if type_wrapper_debug:
218 print ("%s is type %s" % (prop, repr(t)))
219 return SetterWithConversion(prop, _make_args_decoder(t))
220 elif type(t) is dict:
221 assert(len(t) == 1)
222 tkey, tvalue = list(t.items())[0]
223 if type_wrapper_debug:
224 print ("%s is type: %s -> %s" % (prop, repr(tkey), repr(tvalue)))
225 return DictAdderWithConversion(prop, tkey, _make_args_decoder(tvalue))
226 elif type(t) is list:
227 assert(len(t) == 1)
228 if type_wrapper_debug:
229 print ("%s is array of %s" % (prop, repr(t[0])))
230 return ListAdderWithConversion(prop, _make_args_decoder(t[0]))
231 else:
232 raise ValueError("Don't know what to do with %s property '%s' of type %s" % (name, prop, repr(t)))
234 def _create_unmarshaller(name, base_type, object_wrapper = False, property_grabber = _type_properties):
235 all_decorators = {}
236 prop_types = {}
237 settermap = {}
238 if type_wrapper_debug:
239 print ("Wrapping type: %s" % name)
240 print ("-----")
241 for prop, proptype in property_grabber(base_type).items():
242 decorators = []
243 propcmd = '/' + prop
244 if type(proptype) in [list, dict]:
245 if propcmd.endswith('s'):
246 if propcmd.endswith('es'):
247 propcmd = propcmd[:-2]
248 else:
249 propcmd = propcmd[:-1]
250 while isinstance(proptype, PropertyDecorator):
251 decorators.append(proptype)
252 propcmd = proptype.map_cmd(propcmd)
253 proptype = proptype.get_base()
255 settermap[propcmd] = _create_setter(prop, proptype)
256 all_decorators[prop] = decorators
257 prop_types[prop] = proptype
258 base_type.__str__ = lambda self: (str(name) + ":" + " ".join(["%s=%s" % (v.property, str(getattr(self, v.property))) for v in settermap.values()]))
259 if type_wrapper_debug:
260 print ("")
261 def exec_cmds(o):
262 for propname, decorators in all_decorators.items():
263 for decorator in decorators:
264 decorator.execute(propname, prop_types[propname], o)
265 if object_wrapper:
266 return exec_cmds, lambda cmd: (lambda self, *args: new_get_things(base_type(), self.path + cmd, settermap, list(args)))
267 else:
268 return lambda cmd, *args: new_get_things(base_type(), cmd, settermap, list(args))
270 class CboxObjMetaclass(type):
271 """Metaclass that creates Python wrapper classes for various C-side objects.
272 This class is responsible for automatically marshalling and type-checking/converting
273 fields of Status inner class on status() calls."""
274 def __new__(cls, name, bases, namespace, **kwds):
275 status_class = namespace['Status']
276 classfinaliser, cmdwrapper = _create_unmarshaller(name, status_class, True)
277 result = type.__new__(cls, name, bases, namespace, **kwds)
278 classfinaliser(result)
279 result.status = cmdwrapper('/status')
280 return result
283 class NonDocObj(object, metaclass = CboxObjMetaclass):
284 """Root class for all wrapper classes that wrap objects that don't have
285 their own identity/UUID.
286 This covers various singletons and inner objects (e.g. engine in instruments)."""
287 class Status:
288 pass
289 def __init__(self, path):
290 self.path = path
292 def cmd(self, cmd, fb = None, *args):
293 do_cmd(self.path + cmd, fb, list(args))
295 def cmd_makeobj(self, cmd, *args):
296 return Document.map_uuid(GetUUID(self.path + cmd, *args).uuid)
298 def get_things(self, cmd, fields, *args):
299 return GetThings(self.path + cmd, fields, list(args))
301 def get_thing(self, cmd, fieldcmd, type, *args):
302 return get_thing(self.path + cmd, fieldcmd, type, *args)
304 def make_path(self, path):
305 return self.path + path
307 def __str__(self):
308 return "%s<%s>" % (self.__class__.__name__, self.path)
310 class DocObj(NonDocObj):
311 """Root class for all wrapper classes that wrap first-class document objects."""
312 class Status:
313 pass
314 def __init__(self, uuid):
315 NonDocObj.__init__(self, Document.uuid_cmd(uuid, ''))
316 self.uuid = uuid
318 def delete(self):
319 self.cmd("/delete")
321 def __str__(self):
322 return "%s<%s>" % (self.__class__.__name__, self.uuid)
324 class VarPath:
325 def __init__(self, path, args = []):
326 self.path = path
327 self.args = args
328 def plus(self, subpath, *args):
329 return VarPath(self.path if subpath is None else self.path + "/" + subpath, self.args + list(args))
330 def set(self, *values):
331 do_cmd(self.path, None, self.args + list(values))
333 ###############################################################################
334 # And those are the proper user-accessible objects.
335 ###############################################################################
337 class Config:
338 class KeysUnmarshaller:
339 keys = [str]
340 keys_unmarshaller = _create_unmarshaller('Config.keys()', KeysUnmarshaller)
342 """INI file manipulation class."""
343 @staticmethod
344 def sections(prefix = ""):
345 """Return a list of configuration sections."""
346 return [CfgSection(name) for name in get_thing('/config/sections', '/section', [str], prefix)]
348 @staticmethod
349 def keys(section, prefix = ""):
350 """Return a list of configuration keys in a section, with optional prefix filtering."""
351 return Config.keys_unmarshaller('/config/keys', str(section), str(prefix)).keys
353 @staticmethod
354 def get(section, key):
355 """Return a string value of a given key."""
356 return get_thing('/config/get', '/value', str, str(section), str(key))
358 @staticmethod
359 def set(section, key, value):
360 """Set a string value for a given key."""
361 do_cmd('/config/set', None, [str(section), str(key), str(value)])
363 @staticmethod
364 def delete(section, key):
365 """Delete a given key."""
366 do_cmd('/config/delete', None, [str(section), str(key)])
368 @staticmethod
369 def save(filename = None):
370 """Save config, either into current INI file or some other file."""
371 if filename is None:
372 do_cmd('/config/save', None, [])
373 else:
374 do_cmd('/config/save', None, [str(filename)])
376 @staticmethod
377 def add_section(section, content):
378 """Populate a config section based on a string with key=value lists.
379 This is a toy/debug function, it doesn't handle any edge cases."""
380 for line in content.splitlines():
381 line = line.strip()
382 if line == '' or line.startswith('#'):
383 continue
384 try:
385 key, value = line.split("=", 2)
386 except ValueError as err:
387 raise ValueError("Cannot parse config line '%s'" % line)
388 Config.set(section, key.strip(), value.strip())
390 class Transport:
391 @staticmethod
392 def seek_ppqn(ppqn):
393 do_cmd('/master/seek_ppqn', None, [int(ppqn)])
394 @staticmethod
395 def seek_samples(samples):
396 do_cmd('/master/seek_samples', None, [int(samples)])
397 @staticmethod
398 def set_tempo(tempo):
399 do_cmd('/master/set_tempo', None, [float(tempo)])
400 @staticmethod
401 def set_timesig(nom, denom):
402 do_cmd('/master/set_timesig', None, [int(nom), int(denom)])
403 @staticmethod
404 def set_ppqn_factor(factor):
405 do_cmd('/master/set_ppqn_factor', None, [int(factor)])
406 @staticmethod
407 def play():
408 do_cmd('/master/play', None, [])
409 @staticmethod
410 def stop():
411 do_cmd('/master/stop', None, [])
412 @staticmethod
413 def panic():
414 do_cmd('/master/panic', None, [])
415 @staticmethod
416 def status():
417 return GetThings("/master/status", ['pos', 'pos_ppqn', 'tempo', 'timesig', 'sample_rate', 'playing', 'ppqn_factor'], [])
418 @staticmethod
419 def tell():
420 return GetThings("/master/tell", ['pos', 'pos_ppqn', 'playing'], [])
421 @staticmethod
422 def ppqn_to_samples(pos_ppqn):
423 return get_thing("/master/ppqn_to_samples", '/value', int, pos_ppqn)
424 @staticmethod
425 def samples_to_ppqn(pos_samples):
426 return get_thing("/master/samples_to_ppqn", '/value', int, pos_samples)
428 # Currently responsible for both JACK and USB I/O - not all functionality is
429 # supported by both.
430 class JackIO:
431 AUDIO_TYPE = "32 bit float mono audio"
432 MIDI_TYPE = "8 bit raw midi"
433 PORT_IS_SINK = 0x1
434 PORT_IS_SOURCE = 0x2
435 PORT_IS_PHYSICAL = 0x4
436 PORT_CAN_MONITOR = 0x8
437 PORT_IS_TERMINAL = 0x10
438 @staticmethod
439 def status():
440 # Some of these only make sense for
441 return GetThings("/io/status", ['client_type', 'client_name', 'audio_inputs', 'audio_outputs', 'buffer_size', '*midi_output', '*midi_input', 'sample_rate', 'output_resolution', '*usb_midi_input', '*usb_midi_output'], [])
442 @staticmethod
443 def create_midi_input(name, autoconnect_spec = None):
444 uuid = GetUUID("/io/create_midi_input", name).uuid
445 if autoconnect_spec is not None and autoconnect_spec != '':
446 JackIO.autoconnect(uuid, autoconnect_spec)
447 return uuid
448 @staticmethod
449 def create_midi_output(name, autoconnect_spec = None):
450 uuid = GetUUID("/io/create_midi_output", name).uuid
451 if autoconnect_spec is not None and autoconnect_spec != '':
452 JackIO.autoconnect(uuid, autoconnect_spec)
453 return uuid
454 @staticmethod
455 def autoconnect_midi_output(uuid, autoconnect_spec = None):
456 if autoconnect_spec is not None:
457 do_cmd("/io/autoconnect", None, [uuid, autoconnect_spec])
458 else:
459 do_cmd("/io/autoconnect", None, [uuid, ''])
460 autoconnect_midi_input = autoconnect_midi_output
461 @staticmethod
462 def rename_midi_output(uuid, new_name):
463 do_cmd("/io/rename_midi_port", None, [uuid, new_name])
464 rename_midi_input = rename_midi_output
465 @staticmethod
466 def disconnect_midi_output(uuid):
467 do_cmd("/io/disconnect_midi_port", None, [uuid])
468 disconnect_midi_input = disconnect_midi_output
469 @staticmethod
470 def disconnect_midi_output(uuid):
471 do_cmd("/io/disconnect_midi_output", None, [uuid])
472 @staticmethod
473 def delete_midi_input(uuid):
474 do_cmd("/io/delete_midi_input", None, [uuid])
475 @staticmethod
476 def delete_midi_output(uuid):
477 do_cmd("/io/delete_midi_output", None, [uuid])
478 @staticmethod
479 def route_midi_input(input_uuid, scene_uuid):
480 do_cmd("/io/route_midi_input", None, [input_uuid, scene_uuid])
481 @staticmethod
482 def set_appsink_for_midi_input(input_uuid, enabled):
483 do_cmd("/io/set_appsink_for_midi_input", None, [input_uuid, 1 if enabled else 0])
484 @staticmethod
485 def get_new_events(input_uuid):
486 seq = []
487 do_cmd("/io/get_new_events", (lambda cmd, fb, args: seq.append((cmd, fb, args))), [input_uuid])
488 return seq
489 @staticmethod
490 def port_connect(pfrom, pto):
491 do_cmd("/io/port_connect", None, [pfrom, pto])
492 @staticmethod
493 def port_disconnect(pfrom, pto):
494 do_cmd("/io/port_disconnect", None, [pfrom, pto])
495 @staticmethod
496 def get_ports(name_mask = ".*", type_mask = ".*", flag_mask = 0):
497 return get_thing("/io/get_ports", '/port', [str], name_mask, type_mask, int(flag_mask))
498 @staticmethod
499 def get_connected_ports(port):
500 return get_thing("/io/get_connected_ports", '/port', [str], port)
502 def call_on_idle(callback = None):
503 do_cmd("/on_idle", callback, [])
505 def get_new_events():
506 seq = []
507 do_cmd("/on_idle", (lambda cmd, fb, args: seq.append((cmd, fb, args))), [])
508 return seq
510 def send_midi_event(*data, output = None):
511 do_cmd('/send_event_to', None, [output if output is not None else ''] + list(data))
513 def send_sysex(data, output = None):
514 do_cmd('/send_sysex_to', None, [output if output is not None else '', bytearray(data)])
516 class CfgSection:
517 def __init__(self, name):
518 self.name = name
520 def __getitem__(self, key):
521 return Config.get(self.name, key)
523 def __setitem__(self, key, value):
524 Config.set(self.name, key, value)
526 def __delitem__(self, key):
527 Config.delete(self.name, key)
529 def keys(self, prefix = ""):
530 return Config.keys(self.name, prefix)
533 class Pattern:
534 @staticmethod
535 def get_pattern():
536 pat_data = get_thing("/get_pattern", '/pattern', (bytes, int))
537 if pat_data is not None:
538 pat_blob, length = pat_data
539 pat_data = []
540 ofs = 0
541 while ofs < len(pat_blob):
542 data = list(struct.unpack_from("iBBbb", pat_blob, ofs))
543 data[1:2] = []
544 pat_data.append(tuple(data))
545 ofs += 8
546 return pat_data, length
547 return None
549 @staticmethod
550 def serialize_event(time, *data):
551 if len(data) >= 1 and len(data) <= 3:
552 return struct.pack("iBBbb"[0:2 + len(data)], int(time), len(data), *[int(v) for v in data])
553 raise ValueError("Invalid length of an event (%d)" % len(data))
555 class Document:
556 """Document singleton."""
557 classmap = {}
558 objmap = {}
559 @staticmethod
560 def dump():
561 """Print all objects in the documents to stdout. Only used for debugging."""
562 do_cmd("/doc/dump", None, [])
563 @staticmethod
564 def uuid_cmd(uuid, cmd):
565 """Internal: execute a given request on an object with specific UUID."""
566 return "/doc/uuid/%s%s" % (uuid, cmd)
567 @staticmethod
568 def get_uuid(path):
569 """Internal: retrieve an UUID of an object that has specified path."""
570 return GetUUID('%s/get_uuid' % path).uuid
571 @staticmethod
572 def map_path(path, *args):
573 """Internal: return an object corresponding to a path"""
574 return Document.map_uuid(Document.get_uuid(path))
575 @staticmethod
576 def cmd_makeobj(cmd, *args):
577 """Internal: create an object from the UUID result of a command"""
578 return Document.map_uuid(GetUUID(cmd, *args).uuid)
579 @staticmethod
580 def get_obj_class(uuid):
581 """Internal: retrieve an internal class type of an object that has specified path."""
582 return get_thing(Document.uuid_cmd(uuid, "/get_class_name"), '/class_name', str)
583 @staticmethod
584 def get_song():
585 """Retrieve the current song object of a given document. Each document can
586 only have one current song."""
587 return Document.map_path("/song")
588 @staticmethod
589 def get_scene():
590 """Retrieve the first scene object of a default engine. This function
591 is considered obsolete-ish, because of multiple scene support."""
592 return Document.map_path("/scene")
593 @staticmethod
594 def get_engine():
595 """Retrieve the current RT engine object of a given document. Each document can
596 only have one current RT engine."""
597 return Document.map_path("/rt/engine")
598 @staticmethod
599 def get_rt():
600 """Retrieve the RT singleton. RT is an object used to communicate between
601 realtime and user thread, and is currently also used to access the audio
602 engine."""
603 return Document.map_path("/rt")
604 @staticmethod
605 def new_engine(srate, bufsize):
606 """Create a new off-line engine object. This new engine object cannot be used for
607 audio playback - that's only allowed for default engine."""
608 return Document.cmd_makeobj('/new_engine', int(srate), int(bufsize))
609 @staticmethod
610 def map_uuid(uuid):
611 """Create or retrieve a Python-side accessor proxy for a C-side object."""
612 if uuid is None:
613 return None
614 if uuid in Document.objmap:
615 return Document.objmap[uuid]
616 try:
617 oclass = Document.get_obj_class(uuid)
618 except Exception as e:
619 print ("Note: Cannot get class for " + uuid)
620 Document.dump()
621 raise
622 o = Document.classmap[oclass](uuid)
623 Document.objmap[uuid] = o
624 if hasattr(o, 'init_object'):
625 o.init_object()
626 return o
627 @staticmethod
628 def map_uuid_and_check(uuid, t):
629 o = Document.map_uuid(uuid)
630 if not isinstance(o, t):
631 raise TypeError("UUID %s is of type %s, expected %s" % (uuid, o.__class__, t))
632 return o
634 class DocPattern(DocObj):
635 class Status:
636 event_count = int
637 loop_end = int
638 name = str
639 def __init__(self, uuid):
640 DocObj.__init__(self, uuid)
641 def set_name(self, name):
642 self.cmd("/name", None, name)
643 Document.classmap['cbox_midi_pattern'] = DocPattern
645 class ClipItem:
646 def __init__(self, pos, offset, length, pattern, clip):
647 self.pos = pos
648 self.offset = offset
649 self.length = length
650 self.pattern = Document.map_uuid(pattern)
651 self.clip = Document.map_uuid(clip)
652 def __str__(self):
653 return "pos=%d offset=%d length=%d pattern=%s clip=%s" % (self.pos, self.offset, self.length, self.pattern.uuid, self.clip.uuid)
654 def __eq__(self, other):
655 return str(self) == str(other)
657 class DocTrackClip(DocObj):
658 class Status:
659 pos = int
660 offset = int
661 length = int
662 pattern = DocPattern
663 def __init__(self, uuid):
664 DocObj.__init__(self, uuid)
665 Document.classmap['cbox_track_item'] = DocTrackClip
667 class DocTrack(DocObj):
668 class Status:
669 clips = [ClipItem]
670 name = SettableProperty(str)
671 external_output = SettableProperty(str)
672 def add_clip(self, pos, offset, length, pattern):
673 return self.cmd_makeobj("/add_clip", int(pos), int(offset), int(length), pattern.uuid)
674 Document.classmap['cbox_track'] = DocTrack
676 class TrackItem:
677 def __init__(self, name, count, track):
678 self.name = name
679 self.count = count
680 self.track = Document.map_uuid(track)
682 class PatternItem:
683 def __init__(self, name, length, pattern):
684 self.name = name
685 self.length = length
686 self.pattern = Document.map_uuid(pattern)
688 class MtiItem:
689 def __init__(self, pos, tempo, timesig_nom, timesig_denom):
690 self.pos = pos
691 self.tempo = tempo
692 self.timesig_nom = timesig_nom
693 self.timesig_denom = timesig_denom
694 def __eq__(self, o):
695 return self.pos == o.pos and self.tempo == o.tempo and self.timesig_nom == o.timesig_nom and self.timesig_denom == o.timesig_denom
697 class DocSongStatus:
698 tracks = None
699 patterns = None
701 class DocSong(DocObj):
702 class Status:
703 tracks = [TrackItem]
704 patterns = [PatternItem]
705 mtis = [MtiItem]
706 loop_start = int
707 loop_end = int
708 def clear(self):
709 return self.cmd("/clear", None)
710 def set_loop(self, ls, le):
711 return self.cmd("/set_loop", None, int(ls), int(le))
712 def set_mti(self, pos, tempo = None, timesig_nom = None, timesig_denom = None):
713 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)
714 def add_track(self):
715 return self.cmd_makeobj("/add_track")
716 def load_drum_pattern(self, name):
717 return self.cmd_makeobj("/load_pattern", name, 1)
718 def load_drum_track(self, name):
719 return self.cmd_makeobj("/load_track", name, 1)
720 def pattern_from_blob(self, blob, length):
721 return self.cmd_makeobj("/load_blob", bytearray(blob), int(length))
722 def loop_single_pattern(self, loader):
723 self.clear()
724 track = self.add_track()
725 pat = loader()
726 length = pat.status().loop_end
727 track.add_clip(0, 0, length, pat)
728 self.set_loop(0, length)
729 self.update_playback()
730 def update_playback(self):
731 # XXXKF Maybe make it a song-level API instead of global
732 do_cmd("/update_playback", None, [])
733 Document.classmap['cbox_song'] = DocSong
735 class UnknownModule(NonDocObj):
736 class Status:
737 pass
739 class EffectSlot(NonDocObj):
740 class Status:
741 insert_preset = SettableProperty(str)
742 insert_engine = SettableProperty(str)
743 bypass = SettableProperty(bool)
744 def init_object(self):
745 # XXXKF add wrapper classes for effect engines
746 self.engine = UnknownModule(self.path + "/engine")
748 class InstrumentOutput(EffectSlot):
749 class Status(EffectSlot.Status):
750 gain_linear = float
751 gain = float
752 output = int
754 class DocInstrument(DocObj):
755 class Status:
756 name = str
757 outputs = int
758 aux_offset = int
759 engine = str
760 def init_object(self):
761 s = self.status()
762 engine = s.engine
763 if engine in engine_classes:
764 self.engine = engine_classes[engine]("/doc/uuid/" + self.uuid + "/engine")
765 self.output_slots = []
766 for i in range(s.outputs):
767 io = InstrumentOutput(self.make_path('/output/%d' % (i + 1)))
768 io.init_object()
769 self.output_slots.append(io)
770 def move_to(self, target_scene, pos = 0):
771 return self.cmd_makeobj("/move_to", target_scene.uuid, pos + 1)
772 def get_output_slot(self, slot):
773 return self.output_slots[slot]
774 Document.classmap['cbox_instrument'] = DocInstrument
776 class DocLayer(DocObj):
777 class Status:
778 name = str
779 instrument_name = str
780 instrument = AltPropName('/instrument_uuid', DocInstrument)
781 enable = SettableProperty(bool)
782 low_note = SettableProperty(int)
783 high_note = SettableProperty(int)
784 fixed_note = SettableProperty(int)
785 in_channel = SettableProperty(int)
786 out_channel = SettableProperty(int)
787 disable_aftertouch = SettableProperty(bool)
788 invert_sustain = SettableProperty(bool)
789 consume = SettableProperty(bool)
790 ignore_scene_transpose = SettableProperty(bool)
791 ignore_program_changes = SettableProperty(bool)
792 transpose = SettableProperty(int)
793 def get_instrument(self):
794 return self.status().instrument
795 Document.classmap['cbox_layer'] = DocLayer
797 class SamplerEngine(NonDocObj):
798 class Status(object):
799 """Maximum number of voices playing at the same time."""
800 polyphony = int
801 """Current number of voices playing."""
802 active_voices = int
803 """Current number of disk streams."""
804 active_pipes = int
805 """GM volume (14-bit) per MIDI channel."""
806 volume = {int:int}
807 """GM pan (14-bit) per MIDI channel."""
808 pan = {int:int}
809 """Output offset per MIDI channel."""
810 output = {int:int}
811 """Current number of voices playing per MIDI channel."""
812 channel_voices = AltPropName('/channel_voices', {int:int})
813 """MIDI channel -> (program number, program name)"""
814 patches = {int:(int, str)}
816 def load_patch_from_cfg(self, patch_no, cfg_section, display_name):
817 """Load a sampler program from an 'spgm:' config section."""
818 return self.cmd_makeobj("/load_patch", int(patch_no), cfg_section, display_name)
820 def load_patch_from_string(self, patch_no, sample_dir, sfz_data, display_name):
821 """Load a sampler program from a string, using given filesystem path for sample directory."""
822 return self.cmd_makeobj("/load_patch_from_string", int(patch_no), sample_dir, sfz_data, display_name)
824 def load_patch_from_file(self, patch_no, sfz_name, display_name):
825 """Load a sampler program from a filesystem file."""
826 return self.cmd_makeobj("/load_patch_from_file", int(patch_no), sfz_name, display_name)
828 def load_patch_from_tar(self, patch_no, tar_name, sfz_name, display_name):
829 """Load a sampler program from a tar file."""
830 return self.cmd_makeobj("/load_patch_from_file", int(patch_no), "sbtar:%s;%s" % (tar_name, sfz_name), display_name)
832 def set_patch(self, channel, patch_no):
833 """Select patch identified by patch_no in a specified MIDI channel."""
834 self.cmd("/set_patch", None, int(channel), int(patch_no))
835 def set_output(self, channel, output):
836 """Set output offset value in a specified MIDI channel."""
837 self.cmd("/set_output", None, int(channel), int(output))
838 def get_unused_program(self):
839 """Returns first program number that has no program associated with it."""
840 return self.get_thing("/get_unused_program", '/program_no', int)
841 def set_polyphony(self, polyphony):
842 """Set a maximum number of voices that can be played at a given time."""
843 self.cmd("/polyphony", None, int(polyphony))
844 def get_patches(self):
845 """Return a map of program identifiers to program objects."""
846 return self.get_thing("/patches", '/patch', {int : (str, SamplerProgram, int)})
848 class FluidsynthEngine(NonDocObj):
849 class Status:
850 polyphony = int
851 soundfont = str
852 patch = {int: int}
853 def load_soundfont(self, filename):
854 return self.cmd_makeobj("/load_soundfont", filename)
855 def set_patch(self, channel, patch_no):
856 self.cmd("/set_patch", None, int(channel), int(patch_no))
857 def set_polyphony(self, polyphony):
858 self.cmd("/polyphony", None, int(polyphony))
859 def get_patches(self):
860 return self.get_thing("/patches", '/patch', [str])
862 class StreamPlayerEngine(NonDocObj):
863 class Status:
864 filename = str
865 pos = int
866 length = int
867 playing = int
868 def play(self):
869 self.cmd('/play')
870 def stop(self):
871 self.cmd('/stop')
872 def seek(self, place):
873 self.cmd('/seek', None, int(place))
874 def load(self, filename, loop_start = -1):
875 self.cmd('/load', None, filename, int(loop_start))
876 def unload(self):
877 self.cmd('/unload')
879 class TonewheelOrganEngine(NonDocObj):
880 class Status:
881 upper_drawbar = SettableProperty({int: int})
882 lower_drawbar = SettableProperty({int: int})
883 pedal_drawbar = SettableProperty({int: int})
884 upper_vibrato = SettableProperty(bool)
885 lower_vibrato = SettableProperty(bool)
886 vibrato_mode = SettableProperty(int)
887 vibrato_chorus = SettableProperty(int)
888 percussion_enable = SettableProperty(bool)
889 percussion_3rd = SettableProperty(bool)
891 engine_classes = {
892 'sampler' : SamplerEngine,
893 'fluidsynth' : FluidsynthEngine,
894 'stream_player' : StreamPlayerEngine,
895 'tonewheel_organ' : TonewheelOrganEngine,
898 class DocAuxBus(DocObj):
899 class Status:
900 name = str
901 def init_object(self):
902 self.slot = EffectSlot("/doc/uuid/" + self.uuid + "/slot")
903 self.slot.init_object()
905 Document.classmap['cbox_aux_bus'] = DocAuxBus
907 class DocScene(DocObj):
908 class Status:
909 name = str
910 title = str
911 transpose = int
912 layers = [DocLayer]
913 instruments = {str: (str, DocInstrument)}
914 auxes = {str: DocAuxBus}
915 enable_default_song_input = SettableProperty(bool)
916 enable_default_external_input = SettableProperty(bool)
917 def clear(self):
918 self.cmd("/clear", None)
919 def load(self, name):
920 self.cmd("/load", None, name)
921 def load_aux(self, aux):
922 return self.cmd_makeobj("/load_aux", aux)
923 def delete_aux(self, aux):
924 return self.cmd("/delete_aux", None, aux)
925 def delete_layer(self, pos):
926 self.cmd("/delete_layer", None, int(1 + pos))
927 def move_layer(self, old_pos, new_pos):
928 self.cmd("/move_layer", None, int(old_pos + 1), int(new_pos + 1))
930 def add_layer(self, aux, pos = None):
931 if pos is None:
932 return self.cmd_makeobj("/add_layer", 0, aux)
933 else:
934 # Note: The positions in high-level API are zero-based.
935 return self.cmd_makeobj("/add_layer", int(1 + pos), aux)
936 def add_instrument_layer(self, name, pos = None):
937 if pos is None:
938 return self.cmd_makeobj("/add_instrument_layer", 0, name)
939 else:
940 return self.cmd_makeobj("/add_instrument_layer", int(1 + pos), name)
941 def add_new_instrument_layer(self, name, engine, pos = None):
942 if pos is None:
943 return self.cmd_makeobj("/add_new_instrument_layer", 0, name, engine)
944 else:
945 return self.cmd_makeobj("/add_new_instrument_layer", int(1 + pos), name, engine)
946 def send_midi_event(self, *data):
947 self.cmd('/send_event', None, *data)
948 def play_pattern(self, pattern, tempo, id = 0):
949 self.cmd('/play_pattern', None, pattern.uuid, float(tempo), int(id))
950 Document.classmap['cbox_scene'] = DocScene
952 class DocRt(DocObj):
953 class Status:
954 audio_channels = (int, int)
955 state = (int, str)
956 Document.classmap['cbox_rt'] = DocRt
958 class DocModule(DocObj):
959 class Status:
960 pass
961 Document.classmap['cbox_module'] = DocModule
963 class DocEngine(DocObj):
964 class Status:
965 scenes = AltPropName('/scene', [DocScene])
966 def init_object(self):
967 self.master_effect = EffectSlot(self.path + "/master_effect")
968 self.master_effect.init_object()
969 def new_scene(self):
970 return self.cmd_makeobj('/new_scene')
971 def new_recorder(self, filename):
972 return self.cmd_makeobj("/new_recorder", filename)
973 def render_stereo(self, samples):
974 return self.get_thing("/render_stereo", '/data', bytes, samples)
975 Document.classmap['cbox_engine'] = DocEngine
977 class DocRecorder(DocObj):
978 class Status:
979 filename = str
980 Document.classmap['cbox_recorder'] = DocRecorder
982 class SamplerProgram(DocObj):
983 class Status:
984 name = str
985 sample_dir = str
986 source_file = str
987 program_no = int
988 in_use = int
989 def get_regions(self):
990 return self.get_thing("/regions", '/region', [SamplerLayer])
991 def get_groups(self):
992 g = self.get_things("/groups", ['*group', 'default_group'])
993 return [Document.map_uuid(g.default_group)] + list(map(Document.map_uuid, g.group))
994 def get_control_inits(self):
995 return self.get_thing("/control_inits", '/control_init', [(int, int)])
996 def new_group(self):
997 return self.cmd_makeobj("/new_group")
998 def add_control_init(self, controller, value):
999 return self.cmd("/add_control_init", None, controller, value)
1000 # which = -1 -> remove all controllers with that number from the list
1001 def delete_control_init(self, controller, which = 0):
1002 return self.cmd("/delete_control_init", None, controller, which)
1003 def load_file(self, filename, max_size = -1):
1004 """Return an in-memory file corresponding to a given file inside sfbank.
1005 This can be used for things like scripts, images, descriptions etc."""
1006 data = self.get_thing("/load_file", '/data', bytes, filename, max_size)
1007 if data is None:
1008 return data
1009 return BytesIO(data)
1010 def clone_to(self, dest_module, prog_index):
1011 return self.cmd_makeobj('/clone_to', dest_module.uuid, int(prog_index))
1012 Document.classmap['sampler_program'] = SamplerProgram
1014 class SamplerLayer(DocObj):
1015 class Status:
1016 parent_program = SamplerProgram
1017 parent_group = DocObj
1018 def get_children(self):
1019 return self.get_thing("/get_children", '/region', [SamplerLayer])
1020 def as_string(self):
1021 return self.get_thing("/as_string", '/value', str)
1022 def as_string_full(self):
1023 return self.get_thing("/as_string_full", '/value', str)
1024 def set_param(self, key, value):
1025 self.cmd("/set_param", None, key, str(value))
1026 def new_region(self):
1027 return self.cmd_makeobj("/new_region")
1028 Document.classmap['sampler_layer'] = SamplerLayer