from io import BytesIO import struct import sys import traceback try: from _cbox2 import * #local file _cbox2.py import metadata #local file metadata.py except ModuleNotFoundError: from ._cbox2 import * from . import metadata #local file metadata.py type_wrapper_debug = False is_python3 = not sys.version.startswith("2") ############################################################################### # Ugly internals. Please skip this section for your own sanity. ############################################################################### class GetUUID: """An object that calls a C layer command, receives a /uuid callback from it and stores the passed UUID in its uuid attribute. Example use: GetUUID('/command', arg1, arg2...).uuid """ def __init__(self, cmd, *cmd_args): def callback(cmd, fb, args): if cmd == "/uuid" and len(args) == 1: self.uuid = args[0] else: raise ValueException("Unexpected callback: %s" % cmd) self.callback = callback self.uuid = None do_cmd(cmd, self, list(cmd_args)) def __call__(self, *args): self.callback(*args) class GetThings: """A generic callback object that receives various forms of information from C layer and converts then into object's Python attributes. This is an obsolete interface, to be replaced by GetUUID or metaclass based type-safe autoconverter. However, there are still some cases that aren't (yet) handled by either. """ @staticmethod def by_uuid(uuid, cmd, anames, args): return GetThings(Document.uuid_cmd(uuid, cmd), anames, args) def __init__(self, cmd, anames, args): for i in anames: if i.startswith("*"): setattr(self, i[1:], []) elif i.startswith("%"): setattr(self, i[1:], {}) else: setattr(self, i, None) anames = set(anames) self.seq = [] def update_callback(cmd, fb, args): self.seq.append((cmd, fb, args)) cmd = cmd[1:] if cmd in anames: if len(args) == 1: setattr(self, cmd, args[0]) else: setattr(self, cmd, args) elif "*" + cmd in anames: if len(args) == 1: getattr(self, cmd).append(args[0]) else: getattr(self, cmd).append(args) elif "%" + cmd in anames: if len(args) == 2: getattr(self, cmd)[args[0]] = args[1] else: getattr(self, cmd)[args[0]] = args[1:] elif "?" + cmd in anames: setattr(self, cmd, bool(args[0])) elif len(args) == 1: setattr(self, cmd, args[0]) do_cmd(cmd, update_callback, args) def __str__(self): return str(self.seq) class PropertyDecorator(object): """Abstract property decorator.""" def __init__(self, base): self.base = base def get_base(self): return self.base def map_cmd(self, cmd): return cmd class AltPropName(PropertyDecorator): """Command-name-changing property decorator. Binds a property to the specified /path, different from the default one, which based on property name, with -s and -es suffix removed for lists and dicts.""" def __init__(self, alt_name, base): PropertyDecorator.__init__(self, base) self.alt_name = alt_name def map_cmd(self, cmd): return self.alt_name def execute(self, property, proptype, klass): pass class SettableProperty(PropertyDecorator): """Decorator that creates a setter method for the property.""" def execute(self, property, proptype, klass): if type(proptype) is dict: setattr(klass, 'set_' + property, lambda self, key, value: self.cmd('/' + property, None, key, value)) elif type(proptype) is bool: setattr(klass, 'set_' + property, lambda self, value: self.cmd('/' + property, None, 1 if value else 0)) elif issubclass(proptype, DocObj): setattr(klass, 'set_' + property, lambda self, value: self.cmd('/' + property, None, value.uuid)) else: setattr(klass, 'set_' + property, lambda self, value: self.cmd('/' + property, None, proptype(value))) def new_get_things(obj, cmd, settermap, args): """Call C command with arguments 'args', populating a return object obj using settermap to interpret callback commands and initialise the return object.""" def update_callback(cmd2, fb, args2): try: if cmd2 in settermap: settermap[cmd2](obj, args2) elif cmd2 != '/uuid': # Ignore UUID as it's usually safe to do so print ("Unexpected command: %s" % cmd2) except Exception as error: traceback.print_exc() raise # Set initial values for the properties (None or empty dict/list) for setterobj in settermap.values(): setattr(obj, setterobj.property, setterobj.init_value()) # Call command and apply callback commands via setters to the object do_cmd(cmd, update_callback, args) return obj def _error_arg_mismatch(required, passed): raise ValueError("Types required: %s, values passed: %s" % (repr(required), repr(passed))) def _handle_object_wrapping(t): if issubclass(t, DocObj): return lambda uuid: Document.map_uuid_and_check(uuid, t) return t def _make_args_to_type_lambda(t): t = _handle_object_wrapping(t) return lambda args: t(*args) def _make_args_to_tuple_of_types_lambda(ts): ts = list(map(_handle_object_wrapping, ts)) 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) def _make_args_decoder(t): if type(t) is tuple: return _make_args_to_tuple_of_types_lambda(t) else: return _make_args_to_type_lambda(t) def get_thing(cmd, fieldcmd, datatype, *args): pull = False if type(datatype) is list: assert (len(datatype) == 1) decoder = _make_args_decoder(datatype[0]) value = [] def adder(data): value.append(decoder(data)) elif type(datatype) is dict: assert (len(datatype) == 1) key_type, value_type = list(datatype.items())[0] key_decoder = _make_args_decoder(key_type) value_decoder = _make_args_decoder(value_type) value = {} def adder(data): value[key_decoder([data[0]])] = value_decoder(data[1:]) else: decoder = _make_args_decoder(datatype) def adder(data): value[0] = decoder(data) value = [None] pull = True def callback(cmd2, fb, args2): if cmd2 == fieldcmd: adder(args2) else: print ("Unexpected command %s" % cmd2) do_cmd(cmd, callback, list(args)) if pull: return value[0] else: return value class SetterWithConversion: """A setter object class that sets a specific property to a typed value or a tuple of typed value.""" def __init__(self, property, extractor): self.property = property self.extractor = extractor def init_value(self): return None def __call__(self, obj, args): # print ("Setting attr %s on object %s" % (self.property, obj)) setattr(obj, self.property, self.extractor(args)) class ListAdderWithConversion: """A setter object class that adds a tuple filled with type-converted arguments of the callback to a list. E.g. ListAdderWithConversion('foo', (int, int))(obj, [1,2]) adds a tuple: (int(1), int(2)) to the list obj.foo""" def __init__(self, property, extractor): self.property = property self.extractor = extractor def init_value(self): return [] def __call__(self, obj, args): getattr(obj, self.property).append(self.extractor(args)) class DictAdderWithConversion: """A setter object class that adds a tuple filled with type-converted arguments of the callback to a dictionary under a key passed as first argument i.e. DictAdderWithConversion('foo', str, (int, int))(obj, ['bar',1,2]) adds a tuple: (int(1), int(2)) under key 'bar' to obj.foo""" def __init__(self, property, keytype, valueextractor): self.property = property self.keytype = keytype self.valueextractor = valueextractor def init_value(self): return {} def __call__(self, obj, args): getattr(obj, self.property)[self.keytype(args[0])] = self.valueextractor(args[1:]) def _type_properties(base_type): return {prop: getattr(base_type, prop) for prop in dir(base_type) if not prop.startswith("__")} def _create_setter(prop, t): if type(t) in [type, tuple] or issubclass(type(t), DocObj): if type_wrapper_debug: print ("%s is type %s" % (prop, repr(t))) return SetterWithConversion(prop, _make_args_decoder(t)) elif type(t) is dict: assert(len(t) == 1) tkey, tvalue = list(t.items())[0] if type_wrapper_debug: print ("%s is type: %s -> %s" % (prop, repr(tkey), repr(tvalue))) return DictAdderWithConversion(prop, tkey, _make_args_decoder(tvalue)) elif type(t) is list: assert(len(t) == 1) if type_wrapper_debug: print ("%s is array of %s" % (prop, repr(t[0]))) return ListAdderWithConversion(prop, _make_args_decoder(t[0])) else: raise ValueError("Don't know what to do with property '%s' of type %s" % (prop, repr(t))) def _create_unmarshaller(name, base_type, object_wrapper = False, property_grabber = _type_properties): all_decorators = {} prop_types = {} settermap = {} if type_wrapper_debug: print ("Wrapping type: %s" % name) print ("-----") for prop, proptype in property_grabber(base_type).items(): decorators = [] propcmd = '/' + prop if type(proptype) in [list, dict]: if propcmd.endswith('s'): if propcmd.endswith('es'): propcmd = propcmd[:-2] else: propcmd = propcmd[:-1] while isinstance(proptype, PropertyDecorator): decorators.append(proptype) propcmd = proptype.map_cmd(propcmd) proptype = proptype.get_base() settermap[propcmd] = _create_setter(prop, proptype) all_decorators[prop] = decorators prop_types[prop] = proptype base_type.__str__ = lambda self: (str(name) + ":" + " ".join(["%s=%s" % (v.property, str(getattr(self, v.property))) for v in settermap.values()])) if type_wrapper_debug: print ("") def exec_cmds(o): for propname, decorators in all_decorators.items(): for decorator in decorators: decorator.execute(propname, prop_types[propname], o) if object_wrapper: return exec_cmds, lambda cmd: (lambda self, *args: new_get_things(base_type(), self.path + cmd, settermap, list(args))) else: return lambda cmd, *args: new_get_things(base_type(), cmd, settermap, list(args)) class NonDocObj(object): """Root class for all wrapper classes that wrap objects that don't have their own identity/UUID. This covers various singletons and inner objects (e.g. engine in instruments).""" class Status: pass def __init__(self, path): self.path = path def __new__(classObj, *args, **kwargs): if is_python3: result = object.__new__(classObj) result.__init__(*args, **kwargs) else: result = object.__new__(classObj, *args, **kwargs) name = classObj.__name__ if getattr(classObj, 'wrapped_class', None) != name: classfinaliser, cmdwrapper = _create_unmarshaller(name, classObj.Status, object_wrapper = True) classfinaliser(classObj) classObj.status = cmdwrapper('/status') classObj.wrapped_class = name return result def cmd(self, cmd, fb = None, *args): do_cmd(self.path + cmd, fb, list(args)) def cmd_makeobj(self, cmd, *args): return Document.map_uuid(GetUUID(self.path + cmd, *args).uuid) def get_things(self, cmd, fields, *args): return GetThings(self.path + cmd, fields, list(args)) def get_thing(self, cmd, fieldcmd, type, *args): return get_thing(self.path + cmd, fieldcmd, type, *args) def make_path(self, path): return self.path + path def __str__(self): return "%s<%s>" % (self.__class__.__name__, self.path) class DocObj(NonDocObj): """Root class for all wrapper classes that wrap first-class document objects.""" class Status: pass def __init__(self, uuid): NonDocObj.__init__(self, Document.uuid_cmd(uuid, '')) self.uuid = uuid def delete(self): self.cmd("/delete") def __str__(self): return "%s<%s>" % (self.__class__.__name__, self.uuid) class VarPath: def __init__(self, path, args = []): self.path = path self.args = args def plus(self, subpath, *args): return VarPath(self.path if subpath is None else self.path + "/" + subpath, self.args + list(args)) def set(self, *values): do_cmd(self.path, None, self.args + list(values)) ############################################################################### # And those are the proper user-accessible objects. ############################################################################### class Config: class KeysUnmarshaller: keys = [str] keys_unmarshaller = _create_unmarshaller('Config.keys()', KeysUnmarshaller) """INI file manipulation class.""" @staticmethod def sections(prefix = ""): """Return a list of configuration sections.""" return [CfgSection(name) for name in get_thing('/config/sections', '/section', [str], prefix)] @staticmethod def keys(section, prefix = ""): """Return a list of configuration keys in a section, with optional prefix filtering.""" return Config.keys_unmarshaller('/config/keys', str(section), str(prefix)).keys @staticmethod def get(section, key): """Return a string value of a given key.""" return get_thing('/config/get', '/value', str, str(section), str(key)) @staticmethod def set(section, key, value): """Set a string value for a given key.""" do_cmd('/config/set', None, [str(section), str(key), str(value)]) @staticmethod def delete(section, key): """Delete a given key.""" do_cmd('/config/delete', None, [str(section), str(key)]) @staticmethod def save(filename = None): """Save config, either into current INI file or some other file.""" if filename is None: do_cmd('/config/save', None, []) else: do_cmd('/config/save', None, [str(filename)]) @staticmethod def add_section(section, content): """Populate a config section based on a string with key=value lists. This is a toy/debug function, it doesn't handle any edge cases.""" for line in content.splitlines(): line = line.strip() if line == '' or line.startswith('#'): continue try: key, value = line.split("=", 2) except ValueError as err: raise ValueError("Cannot parse config line '%s'" % line) Config.set(section, key.strip(), value.strip()) class Transport: @staticmethod def seek_ppqn(ppqn): do_cmd('/master/seek_ppqn', None, [int(ppqn)]) @staticmethod def seek_samples(samples): do_cmd('/master/seek_samples', None, [int(samples)]) @staticmethod def set_tempo(tempo): do_cmd('/master/set_tempo', None, [float(tempo)]) @staticmethod def set_timesig(nom, denom): do_cmd('/master/set_timesig', None, [int(nom), int(denom)]) @staticmethod def set_ppqn_factor(factor): do_cmd('/master/set_ppqn_factor', None, [int(factor)]) @staticmethod def play(): do_cmd('/master/play', None, []) @staticmethod def stop(): do_cmd('/master/stop', None, []) @staticmethod def panic(): do_cmd('/master/panic', None, []) @staticmethod def status(): return GetThings("/master/status", ['pos', 'pos_ppqn', 'tempo', 'timesig', 'sample_rate', 'playing', 'ppqn_factor'], []) @staticmethod def tell(): return GetThings("/master/tell", ['pos', 'pos_ppqn', 'playing'], []) @staticmethod def ppqn_to_samples(pos_ppqn): return get_thing("/master/ppqn_to_samples", '/value', int, pos_ppqn) @staticmethod def samples_to_ppqn(pos_samples): return get_thing("/master/samples_to_ppqn", '/value', int, pos_samples) # Currently responsible for both JACK and USB I/O - not all functionality is # supported by both. class JackIO: AUDIO_TYPE = "32 bit float mono audio" MIDI_TYPE = "8 bit raw midi" PORT_IS_SINK = 0x1 PORT_IS_SOURCE = 0x2 PORT_IS_PHYSICAL = 0x4 PORT_CAN_MONITOR = 0x8 PORT_IS_TERMINAL = 0x10 metadata.get_thing = get_thing #avoid circular dependency and redundant code Metadata = metadata.Metadata #use with cbox.JackIO.Metadata.get_all_properties() @staticmethod def status(): # Some of these only make sense for JACK 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', '?external_tempo'], []) @staticmethod def jack_transport_position(): # Some of these only make sense for JACK return GetThings("/io/jack_transport_position", ['state', 'unique_lo', 'unique_hi', 'usecs_lo', 'usecs_hi', 'frame_rate', 'frame', 'bar', 'beat', 'tick', 'bar_start_tick', 'bbt_frame_offset', 'beats_per_bar', 'beat_type', 'ticks_per_beat', 'beats_per_minute', 'is_master'], []) @staticmethod def jack_transport_locate(pos): do_cmd("/io/jack_transport_locate", None, [pos]) @staticmethod def transport_mode(master = True, conditional = False): if master: do_cmd("/io/transport_mode", None, [1 if conditional else 2]) else: do_cmd("/io/transport_mode", None, [0]) @staticmethod def create_midi_input(name, autoconnect_spec = None): uuid = GetUUID("/io/create_midi_input", name).uuid if autoconnect_spec is not None and autoconnect_spec != '': JackIO.autoconnect(uuid, autoconnect_spec) return uuid @staticmethod def create_midi_output(name, autoconnect_spec = None): uuid = GetUUID("/io/create_midi_output", name).uuid if autoconnect_spec is not None and autoconnect_spec != '': JackIO.autoconnect(uuid, autoconnect_spec) return uuid @staticmethod def autoconnect(uuid, autoconnect_spec = None): if autoconnect_spec is not None: do_cmd("/io/autoconnect", None, [uuid, autoconnect_spec]) else: do_cmd("/io/autoconnect", None, [uuid, '']) autoconnect_midi_input = autoconnect autoconnect_midi_output = autoconnect autoconnect_audio_output = autoconnect @staticmethod def rename_midi_output(uuid, new_name): do_cmd("/io/rename_midi_port", None, [uuid, new_name]) rename_midi_input = rename_midi_output @staticmethod def disconnect_midi_port(uuid): do_cmd("/io/disconnect_midi_port", None, [uuid]) @staticmethod def disconnect_midi_output(uuid): do_cmd("/io/disconnect_midi_output", None, [uuid]) @staticmethod def disconnect_midi_input(uuid): do_cmd("/io/disconnect_midi_input", None, [uuid]) @staticmethod def delete_midi_input(uuid): do_cmd("/io/delete_midi_input", None, [uuid]) @staticmethod def delete_midi_output(uuid): do_cmd("/io/delete_midi_output", None, [uuid]) @staticmethod def route_midi_input(input_uuid, scene_uuid): do_cmd("/io/route_midi_input", None, [input_uuid, scene_uuid]) @staticmethod def set_appsink_for_midi_input(input_uuid, enabled): do_cmd("/io/set_appsink_for_midi_input", None, [input_uuid, 1 if enabled else 0]) @staticmethod def get_new_events(input_uuid): seq = [] do_cmd("/io/get_new_events", (lambda cmd, fb, args: seq.append((cmd, fb, args))), [input_uuid]) return seq @staticmethod def create_audio_output(name, autoconnect_spec = None): uuid = GetUUID("/io/create_audio_output", name).uuid if autoconnect_spec is not None and autoconnect_spec != '': JackIO.autoconnect(uuid, autoconnect_spec) return uuid @staticmethod def create_audio_output_router(uuid_left, uuid_right): return get_thing("/io/create_audio_output_router", "/uuid", DocRecorder, uuid_left, uuid_right) @staticmethod def delete_audio_output(uuid): do_cmd("/io/delete_audio_output", None, [uuid]) @staticmethod def rename_audio_output(uuid, new_name): do_cmd("/io/rename_audio_port", None, [uuid, new_name]) @staticmethod def disconnect_audio_output(uuid): do_cmd("/io/disconnect_audio_output", None, [uuid]) @staticmethod def port_connect(pfrom, pto): do_cmd("/io/port_connect", None, [pfrom, pto]) @staticmethod def port_disconnect(pfrom, pto): do_cmd("/io/port_disconnect", None, [pfrom, pto]) @staticmethod def get_ports(name_mask = ".*", type_mask = ".*", flag_mask = 0): return get_thing("/io/get_ports", '/port', [str], name_mask, type_mask, int(flag_mask)) @staticmethod def get_connected_ports(port): return get_thing("/io/get_connected_ports", '/port', [str], port) @staticmethod def external_tempo(enable): """Enable reacting to JACK transport tempo""" do_cmd('/io/external_tempo', None, [1 if enable else 0]) def call_on_idle(callback = None): do_cmd("/on_idle", callback, []) def get_new_events(): seq = [] do_cmd("/on_idle", (lambda cmd, fb, args: seq.append((cmd, fb, args))), []) return seq def send_midi_event(*data, **kwargs): output = kwargs.get('output', None) do_cmd('/send_event_to', None, [output if output is not None else ''] + list(data)) def send_sysex(data, output = None): do_cmd('/send_sysex_to', None, [output if output is not None else '', bytearray(data)]) def flush_rt(): do_cmd('/rt/flush', None, []) class CfgSection: def __init__(self, name): self.name = name def __getitem__(self, key): return Config.get(self.name, key) def __setitem__(self, key, value): Config.set(self.name, key, value) def __delitem__(self, key): Config.delete(self.name, key) def keys(self, prefix = ""): return Config.keys(self.name, prefix) class Pattern: @staticmethod def get_pattern(): pat_data = get_thing("/get_pattern", '/pattern', (bytes, int)) if pat_data is not None: pat_blob, length = pat_data pat_data = [] ofs = 0 while ofs < len(pat_blob): data = list(struct.unpack_from("iBBbb", pat_blob, ofs)) data[1:2] = [] pat_data.append(tuple(data)) ofs += 8 return pat_data, length return None @staticmethod def serialize_event(time, *data): if len(data) >= 1 and len(data) <= 3: return struct.pack("iBBbb"[0:2 + len(data)], int(time), len(data), *[int(v) for v in data]) raise ValueError("Invalid length of an event (%d)" % len(data)) class Document: """Document singleton.""" classmap = {} objmap = {} @staticmethod def dump(): """Print all objects in the documents to stdout. Only used for debugging.""" do_cmd("/doc/dump", None, []) @staticmethod def uuid_cmd(uuid, cmd): """Internal: execute a given request on an object with specific UUID.""" return "/doc/uuid/%s%s" % (uuid, cmd) @staticmethod def get_uuid(path): """Internal: retrieve an UUID of an object that has specified path.""" return GetUUID('%s/get_uuid' % path).uuid @staticmethod def map_path(path, *args): """Internal: return an object corresponding to a path""" return Document.map_uuid(Document.get_uuid(path)) @staticmethod def cmd_makeobj(cmd, *args): """Internal: create an object from the UUID result of a command""" return Document.map_uuid(GetUUID(cmd, *args).uuid) @staticmethod def get_obj_class(uuid): """Internal: retrieve an internal class type of an object that has specified path.""" return get_thing(Document.uuid_cmd(uuid, "/get_class_name"), '/class_name', str) @staticmethod def get_song(): """Retrieve the current song object of a given document. Each document can only have one current song.""" return Document.map_path("/song") @staticmethod def get_scene(): """Retrieve the first scene object of a default engine. This function is considered obsolete-ish, because of multiple scene support.""" return Document.map_path("/scene") @staticmethod def get_engine(): """Retrieve the current RT engine object of a given document. Each document can only have one current RT engine.""" return Document.map_path("/rt/engine") @staticmethod def get_rt(): """Retrieve the RT singleton. RT is an object used to communicate between realtime and user thread, and is currently also used to access the audio engine.""" return Document.map_path("/rt") @staticmethod def new_engine(srate, bufsize): """Create a new off-line engine object. This new engine object cannot be used for audio playback - that's only allowed for default engine.""" return Document.cmd_makeobj('/new_engine', int(srate), int(bufsize)) @staticmethod def map_uuid(uuid): """Create or retrieve a Python-side accessor proxy for a C-side object.""" if uuid is None: return None if uuid in Document.objmap: return Document.objmap[uuid] try: oclass = Document.get_obj_class(uuid) except Exception as e: print ("Note: Cannot get class for " + uuid) Document.dump() raise o = Document.classmap[oclass](uuid) Document.objmap[uuid] = o if hasattr(o, 'init_object'): o.init_object() return o @staticmethod def map_uuid_and_check(uuid, t): o = Document.map_uuid(uuid) if not isinstance(o, t): raise TypeError("UUID %s is of type %s, expected %s" % (uuid, o.__class__, t)) return o class DocPattern(DocObj): class Status: event_count = int loop_end = int name = str def __init__(self, uuid): DocObj.__init__(self, uuid) def set_name(self, name): self.cmd("/name", None, name) Document.classmap['cbox_midi_pattern'] = DocPattern class ClipItem: def __init__(self, pos, offset, length, pattern, clip): self.pos = pos self.offset = offset self.length = length self.pattern = Document.map_uuid(pattern) self.clip = Document.map_uuid(clip) def __str__(self): return "pos=%d offset=%d length=%d pattern=%s clip=%s" % (self.pos, self.offset, self.length, self.pattern.uuid, self.clip.uuid) def __eq__(self, other): return str(self) == str(other) class DocTrackClip(DocObj): class Status: pos = SettableProperty(int) offset = SettableProperty(int) length = SettableProperty(int) pattern = SettableProperty(DocPattern) def __init__(self, uuid): DocObj.__init__(self, uuid) Document.classmap['cbox_track_item'] = DocTrackClip class DocTrack(DocObj): class Status: clips = [ClipItem] name = SettableProperty(str) external_output = SettableProperty(str) mute = SettableProperty(int) def add_clip(self, pos, offset, length, pattern): return self.cmd_makeobj("/add_clip", int(pos), int(offset), int(length), pattern.uuid) def clear_clips(self): return self.cmd_makeobj("/clear_clips") Document.classmap['cbox_track'] = DocTrack class TrackItem: def __init__(self, name, count, track): self.name = name self.count = count self.track = Document.map_uuid(track) class PatternItem: def __init__(self, name, length, pattern): self.name = name self.length = length self.pattern = Document.map_uuid(pattern) class MtiItem: def __init__(self, pos, tempo, timesig_num, timesig_denom): self.pos = pos self.tempo = tempo # Original misspelling self.timesig_num = timesig_num self.timesig_denom = timesig_denom def __getattr__(self, name): if name == 'timesig_nom': return self.timesig_num raise AttributeError(name) def __setattr__(self, name, value): if name == 'timesig_nom': self.timesig_num = value else: self.__dict__[name] = value def __eq__(self, o): return self.pos == o.pos and self.tempo == o.tempo and self.timesig_num == o.timesig_num and self.timesig_denom == o.timesig_denom def __repr__(self): return ("pos: {}, bpm: {}, timesig: {}/{}".format(self.pos, self.tempo, self.timesig_num, self.timesig_denom)) class DocSongStatus: tracks = None patterns = None class DocSong(DocObj): class Status: tracks = [TrackItem] patterns = [PatternItem] mtis = [MtiItem] loop_start = int loop_end = int def clear(self): return self.cmd("/clear", None) def set_loop(self, ls, le): return self.cmd("/set_loop", None, int(ls), int(le)) def set_mti(self, pos, tempo = None, timesig_num = None, timesig_denom = None, timesig_nom = None): if timesig_nom is not None: timesig_num = timesig_nom self.cmd("/set_mti", None, int(pos), float(tempo) if tempo is not None else -1.0, int(timesig_num) if timesig_num is not None else -1, int(timesig_denom) if timesig_denom else -1) def delete_mti(self, pos): """Deleting works only if we set everything to exactly 0. Not None, not -1""" self.set_mti(pos, tempo = 0, timesig_num = 0, timesig_denom = 0, timesig_nom = 0) def add_track(self): return self.cmd_makeobj("/add_track") def load_drum_pattern(self, name): return self.cmd_makeobj("/load_pattern", name, 1) def load_drum_track(self, name): return self.cmd_makeobj("/load_track", name, 1) def pattern_from_blob(self, blob, length): return self.cmd_makeobj("/load_blob", bytearray(blob), int(length)) def loop_single_pattern(self, loader): self.clear() track = self.add_track() pat = loader() length = pat.status().loop_end track.add_clip(0, 0, length, pat) self.set_loop(0, length) self.update_playback() def update_playback(self): # XXXKF Maybe make it a song-level API instead of global do_cmd("/update_playback", None, []) Document.classmap['cbox_song'] = DocSong class UnknownModule(NonDocObj): class Status: pass class DocRecorder(DocObj): class Status: filename = str gain = SettableProperty(float) Document.classmap['cbox_recorder'] = DocRecorder class RecSource(NonDocObj): class Status: handler = [DocRecorder] def attach(self, recorder): self.cmd('/attach', None, recorder.uuid) def detach(self, recorder): self.cmd('/detach', None, recorder.uuid) class EffectSlot(NonDocObj): class Status: insert_preset = SettableProperty(str) insert_engine = SettableProperty(str) bypass = SettableProperty(bool) def init_object(self): # XXXKF add wrapper classes for effect engines self.engine = UnknownModule(self.path + "/engine") class InstrumentOutput(EffectSlot): class Status(EffectSlot.Status): gain_linear = float gain = float output = SettableProperty(int) def init_object(self): EffectSlot.init_object(self) self.rec_dry = RecSource(self.make_path('/rec_dry')) self.rec_wet = RecSource(self.make_path('/rec_wet')) class DocInstrument(DocObj): class Status: name = str outputs = int aux_offset = int engine = str def init_object(self): s = self.status() engine = s.engine if engine in engine_classes: self.engine = engine_classes[engine]("/doc/uuid/" + self.uuid + "/engine") else: raise ValueError("Unknown engine %s" % engine) self.output_slots = [] for i in range(s.outputs): io = InstrumentOutput(self.make_path('/output/%d' % (i + 1))) io.init_object() self.output_slots.append(io) def move_to(self, target_scene, pos = 0): return self.cmd_makeobj("/move_to", target_scene.uuid, pos + 1) def get_output_slot(self, slot): return self.output_slots[slot] Document.classmap['cbox_instrument'] = DocInstrument class DocLayer(DocObj): class Status: name = str instrument_name = str instrument = AltPropName('/instrument_uuid', DocInstrument) enable = SettableProperty(bool) low_note = SettableProperty(int) high_note = SettableProperty(int) fixed_note = SettableProperty(int) in_channel = SettableProperty(int) out_channel = SettableProperty(int) disable_aftertouch = SettableProperty(bool) invert_sustain = SettableProperty(bool) consume = SettableProperty(bool) ignore_scene_transpose = SettableProperty(bool) ignore_program_changes = SettableProperty(bool) transpose = SettableProperty(int) external_output = SettableProperty(str) def get_instrument(self): return self.status().instrument Document.classmap['cbox_layer'] = DocLayer class SamplerEngine(NonDocObj): class Status(object): """Maximum number of voices playing at the same time.""" polyphony = int """Current number of voices playing.""" active_voices = int """Current number of delayed-startup voices waiting to be played.""" active_prevoices = int """Current number of disk streams.""" active_pipes = int """GM volume (14-bit) per MIDI channel.""" volume = {int:int} """GM pan (14-bit) per MIDI channel.""" pan = {int:int} """Output offset per MIDI channel.""" output = {int:int} """Current number of voices playing per MIDI channel.""" channel_voices = AltPropName('/channel_voices', {int:int}) """Current number of voices waiting to be played per MIDI channel.""" channel_prevoices = AltPropName('/channel_prevoices', {int:int}) """MIDI channel -> (program number, program name)""" patches = {int:(int, str)} def load_patch_from_cfg(self, patch_no, cfg_section, display_name): """Load a sampler program from an 'spgm:' config section.""" return self.cmd_makeobj("/load_patch", int(patch_no), cfg_section, display_name) def load_patch_from_string(self, patch_no, sample_dir, sfz_data, display_name): """Load a sampler program from a string, using given filesystem path for sample directory.""" return self.cmd_makeobj("/load_patch_from_string", int(patch_no), sample_dir, sfz_data, display_name) def load_patch_from_file(self, patch_no, sfz_name, display_name): """Load a sampler program from a filesystem file.""" return self.cmd_makeobj("/load_patch_from_file", int(patch_no), sfz_name, display_name) def load_patch_from_tar(self, patch_no, tar_name, sfz_name, display_name): """Load a sampler program from a tar file.""" return self.cmd_makeobj("/load_patch_from_file", int(patch_no), "sbtar:%s;%s" % (tar_name, sfz_name), display_name) def set_patch(self, channel, patch_no): """Select patch identified by patch_no in a specified MIDI channel.""" self.cmd("/set_patch", None, int(channel), int(patch_no)) def set_output(self, channel, output): """Set output offset value in a specified MIDI channel.""" self.cmd("/set_output", None, int(channel), int(output)) def get_unused_program(self): """Returns first program number that has no program associated with it.""" return self.get_thing("/get_unused_program", '/program_no', int) def set_polyphony(self, polyphony): """Set a maximum number of voices that can be played at a given time.""" self.cmd("/polyphony", None, int(polyphony)) def get_patches(self): """Return a map of program identifiers to program objects.""" return self.get_thing("/patches", '/patch', {int : (str, SamplerProgram, int)}) def get_keyswitch_state(self, channel, group): """Return a map of program identifiers to program objects.""" return self.get_thing("/keyswitch_state", '/last_key', int, channel, group) class FluidsynthEngine(NonDocObj): class Status: polyphony = int soundfont = str patch = {int: (int, str)} def load_soundfont(self, filename): return self.cmd_makeobj("/load_soundfont", filename) def set_patch(self, channel, patch_no): self.cmd("/set_patch", None, int(channel), int(patch_no)) def set_polyphony(self, polyphony): self.cmd("/polyphony", None, int(polyphony)) def get_patches(self): return self.get_thing("/patches", '/patch', {int: str}) class StreamPlayerEngine(NonDocObj): class Status: filename = str pos = int length = int playing = int def play(self): self.cmd('/play') def stop(self): self.cmd('/stop') def seek(self, place): self.cmd('/seek', None, int(place)) def load(self, filename, loop_start = -1): self.cmd('/load', None, filename, int(loop_start)) def unload(self): self.cmd('/unload') class TonewheelOrganEngine(NonDocObj): class Status: upper_drawbar = SettableProperty({int: int}) lower_drawbar = SettableProperty({int: int}) pedal_drawbar = SettableProperty({int: int}) upper_vibrato = SettableProperty(bool) lower_vibrato = SettableProperty(bool) vibrato_mode = SettableProperty(int) vibrato_chorus = SettableProperty(int) percussion_enable = SettableProperty(bool) percussion_3rd = SettableProperty(bool) class JackInputEngine(NonDocObj): class Status: inputs = (int, int) engine_classes = { 'sampler' : SamplerEngine, 'fluidsynth' : FluidsynthEngine, 'stream_player' : StreamPlayerEngine, 'tonewheel_organ' : TonewheelOrganEngine, 'jack_input' : JackInputEngine, } class DocAuxBus(DocObj): class Status: name = str def init_object(self): self.slot = EffectSlot("/doc/uuid/" + self.uuid + "/slot") self.slot.init_object() Document.classmap['cbox_aux_bus'] = DocAuxBus class DocScene(DocObj): class Status: name = str title = str transpose = int layers = [DocLayer] instruments = {str: (str, DocInstrument)} auxes = {str: DocAuxBus} enable_default_song_input = SettableProperty(bool) enable_default_external_input = SettableProperty(bool) def clear(self): self.cmd("/clear", None) def load(self, name): self.cmd("/load", None, name) def load_aux(self, aux): return self.cmd_makeobj("/load_aux", aux) def delete_aux(self, aux): return self.cmd("/delete_aux", None, aux) def delete_layer(self, pos): self.cmd("/delete_layer", None, int(1 + pos)) def move_layer(self, old_pos, new_pos): self.cmd("/move_layer", None, int(old_pos + 1), int(new_pos + 1)) #Layer positions are 0 for "append" and other positions are 1...n which need to be unique def add_layer(self, aux, pos = None): if pos is None: return self.cmd_makeobj("/add_layer", 0, aux) else: # Note: The positions in high-level API are zero-based. return self.cmd_makeobj("/add_layer", int(1 + pos), aux) def add_instrument_layer(self, name, pos = None): if pos is None: return self.cmd_makeobj("/add_instrument_layer", 0, name) else: return self.cmd_makeobj("/add_instrument_layer", int(1 + pos), name) def add_new_instrument_layer(self, name, engine, pos = None): if pos is None: return self.cmd_makeobj("/add_new_instrument_layer", 0, name, engine) else: return self.cmd_makeobj("/add_new_instrument_layer", int(1 + pos), name, engine) def add_new_midi_layer(self, ext_output_uuid, pos = None): if pos is None: return self.cmd_makeobj("/add_midi_layer", 0, ext_output_uuid) else: return self.cmd_makeobj("/add_midi_layer", int(1 + pos), ext_output_uuid) def send_midi_event(self, *data): self.cmd('/send_event', None, *data) def play_pattern(self, pattern, tempo, id = 0): self.cmd('/play_pattern', None, pattern.uuid, float(tempo), int(id)) Document.classmap['cbox_scene'] = DocScene class DocRt(DocObj): class Status: audio_channels = (int, int) state = (int, str) Document.classmap['cbox_rt'] = DocRt class DocModule(DocObj): class Status: pass Document.classmap['cbox_module'] = DocModule class DocEngine(DocObj): class Status: scenes = AltPropName('/scene', [DocScene]) def init_object(self): self.master_effect = EffectSlot(self.path + "/master_effect") self.master_effect.init_object() def new_scene(self): return self.cmd_makeobj('/new_scene') def new_recorder(self, filename): return self.cmd_makeobj("/new_recorder", filename) def render_stereo(self, samples): return self.get_thing("/render_stereo", '/data', bytes, samples) Document.classmap['cbox_engine'] = DocEngine class SamplerProgram(DocObj): class Status: name = str sample_dir = str source_file = str program_no = int in_use = int def get_regions(self): return self.get_thing("/regions", '/region', [SamplerLayer]) def get_global(self): return self.cmd_makeobj("/global") def get_hierarchy(self): """see SamplerLayer.get_hierarchy""" return {self.get_global() : self.get_global().get_hierarchy()} def get_control_inits(self): return self.get_thing("/control_inits", '/control_init', [(int, int)]) def get_control_labels(self): return self.get_thing("/control_labels", '/control_label', {int : str}) def get_key_labels(self): return self.get_thing("/key_labels", '/key_label', {int : str}) def get_output_labels(self): return self.get_thing("/output_labels", '/output_label', {int : str}) def get_keyswitch_groups(self): return self.get_thing("/keyswitch_groups", '/key_range', [(int, int)]) def new_group(self): # Obsolete return self.cmd_makeobj("/new_group") def add_control_init(self, controller, value): return self.cmd("/add_control_init", None, controller, value) def add_control_label(self, controller, label): return self.cmd("/add_control_label", None, controller, label) # which = -1 -> remove all controllers with that number from the list def delete_control_init(self, controller, which = 0): return self.cmd("/delete_control_init", None, controller, which) def load_file(self, filename, max_size = -1): """Return an in-memory file corresponding to a given file inside sfbank. This can be used for things like scripts, images, descriptions etc.""" data = self.get_thing("/load_file", '/data', bytes, filename, max_size) if data is None: return data return BytesIO(data) def clone_to(self, dest_module, prog_index): return self.cmd_makeobj('/clone_to', dest_module.uuid, int(prog_index)) Document.classmap['sampler_program'] = SamplerProgram class SamplerLayer(DocObj): class Status: parent_program = SamplerProgram parent = DocObj level = str def get_children(self): """Return all children SamplerLayer. The hierarchy is always global-master-group-region Will be empty if this is an sfz , which has no further children. """ return self.get_thing("/get_children", '/child', [SamplerLayer]) def get_hierarchy(self): """Returns either a level of hierarchy, e.g. or or None, if this is a childless layer, such as a . The hierarchy is always global-master-group-region. Regions alre always on the fourth level. But not all levels might have regions. Hint: Print with pprint during development.""" children = self.get_children() if children: result = {} for childLayer in children: result[childLayer] = childLayer.get_hierarchy() else: result = None return result def as_dict(self): """Returns a dictionary of parameters set at this level of the layer hierarchy.""" return self.get_thing("/as_list", '/value', {str: str}) def as_dict_full(self): """Returns a dictionary of parameters set either at this level of the layer hierarchy or at one of the ancestors.""" return self.get_thing("/as_list_full", '/value', {str: str}) def as_string(self): """A space separated string of all sampler values at this level in the hierarchy, for example ampeg_decay. This only includes non-default values, e.g. from the sfz file""" return self.get_thing("/as_string", '/value', str) def as_string_full(self): """A space separated string of all sampler values at this level in the hierarchy, for example ampeg_decay. This includes all default values. To access the values as dict with number data types use get_params_full(). '_oncc1' will be converted to '_cc1' """ return self.get_thing("/as_string_full", '/value', str) def set_param(self, key, value): self.cmd("/set_param", None, key, str(value)) def unset_param(self, key): self.cmd("/unset_param", None, key) def new_child(self): return self.cmd_makeobj("/new_child") Document.classmap['sampler_layer'] = SamplerLayer