Source code for rv.modules.metamodule

import re
from io import BytesIO
from itertools import chain
from string import digits
from struct import pack

import rv
from rv.chunks import ArrayChunk
from rv.controller import Controller, Range
from rv.modules import Behavior as B
from rv.modules import Module
from rv.modules.base.metamodule import BaseMetaModule
from rv.project import Project
from rv.readers.reader import read_sunvox_file
from slugify import slugify_unicode

USER_DEFINED_RE = re.compile(r"user_defined_\d+")

def slugify(s):
    s = slugify_unicode(s, separator="_", to_lower=True)
    if s == "":
        return "_"
    if s[0] in digits:
        s = f"_{s}"
    return s

class UserDefined(Controller):
    label = None

    def __init__(self, number): = "user_defined_{}".format(number + 1)
        self.number = number + 6
        super().__init__((0, 32768), 0, attached=False)

    def attach(self, instance):
        self._attached = True

    def detach(self, instance):
        self._attached = False

class UserDefinedProxy(Controller):
    def __init__(self, index):
        self.index = index
        super(UserDefinedProxy, self).__init__((0, 32768), 0)

    def __get__(self, instance, owner):
        if instance is None:
            return self
            ctl = instance.user_defined[self.index]
            return ctl.__get__(instance, owner)

    def __set__(self, instance, value):
        if instance is not None:
            ctl = instance.user_defined[self.index]
            return ctl.__set__(instance, value)

    def controller(self, instance):
        return instance.user_defined[self.index]

    def attached(self, instance):
        return self.controller(instance).attached(instance)

    def attach(self, instance):
        return self.controller(instance).attach(instance)

    def detach(self, instance):
        return self.controller(instance).detach(instance)

    def instance_value_type(self, instance):
        return self.controller(instance).value_type

[docs]class MetaModule(BaseMetaModule, Module): """ In addition to standard controllers, you can assign zero or more user-defined controllers which map to module/controller pairs in the project embedded within the MetaModule. """ options_chnm = 0x02 behaviors = {B.receives_audio, B.receives_notes, B.sends_audio, B.sends_notes} class Mapping: def __init__(self, value): self.module, self.controller = value[0], value[1] class MappingArray(ArrayChunk): chnm = 1 length = 64 type = "HH" element_size = 2 * 2 def default(self, _): return MetaModule.Mapping((0, 0)) @property def encoded_values(self): return list( chain.from_iterable((x.module, x.controller) for x in self.values) ) @property def python_type(self): return MetaModule.Mapping @staticmethod def update_user_defined_controllers(metamodule): project = metamodule.project items = zip(metamodule.mappings.values, metamodule.user_defined) for mapping, user_defined_controller in items: if mapping.module == 0 or mapping.module >= len(project.modules): continue mod = project.modules[mapping.module] if not mod: continue controller_index = mapping.controller - 1 controller = list(mod.controllers.values())[controller_index] user_defined_controller.value_type = controller.instance_value_type(mod) user_defined_controller.default = controller.default metamodule.controller_values[ ] = mod.controller_values[] ( user_defined_1, user_defined_2, user_defined_3, user_defined_4, user_defined_5, user_defined_6, user_defined_7, user_defined_8, user_defined_9, user_defined_10, user_defined_11, user_defined_12, user_defined_13, user_defined_14, user_defined_15, user_defined_16, user_defined_17, user_defined_18, user_defined_19, user_defined_20, user_defined_21, user_defined_22, user_defined_23, user_defined_24, user_defined_25, user_defined_26, user_defined_27, ) = [UserDefinedProxy(__i) for __i in range(27)] def __init__(self, **kwargs): project = kwargs.get("project", None) self.user_defined = [ UserDefined(i) for i in range(MAX_USER_DEFINED_CONTROLLERS) ] super(MetaModule, self).__init__(**kwargs) self.mappings = self.MappingArray() self.project = project if project else Project() self.project.metamodule = self def __getattr__(self, key): if USER_DEFINED_RE.match(key): ctl = self.controllers[key] return ctl.__get__(self, None) else: if "user_defined" in self.__dict__: try: i = self.user_defined_aliases.index(key) except ValueError: raise AttributeError() else: ctl_name = list(self.controllers)[i + 5] ctl = self.controllers[ctl_name] return ctl.__get__(self, None) else: raise AttributeError() def __setattr__(self, key, value): if USER_DEFINED_RE.match(key): ctl = self.controllers[key] return ctl.__set__(self, value) else: try: i = self.user_defined_aliases.index(key) except ValueError: super().__setattr__(key, value) else: ctl_name = list(self.controllers)[i + 5] ctl = self.controllers[ctl_name] return ctl.__set__(self, value) def __dir__(self): return list(super().__dir__()) + [ name for name in self.user_defined_aliases if name ] @property def chnk(self): return 8 + self.user_defined_controllers @property def user_defined_aliases(self): return ( [ "u_{}".format(slugify(ctl.label).lower()) if ctl.label else None for ctl in self.user_defined if ctl.attached(self) ] if hasattr(self, "user_defined") else [] ) def on_controller_changed(self, controller, value, down, up): if isinstance(controller, UserDefined) and down: mapping_index = controller.number - self.user_defined[0].number mapping = self.mappings.values[mapping_index] mod = self.project.modules[mapping.module] controller_index = mapping.controller - 1 controllers = list(mod.controllers.items()) ctl_name, ctl = controllers[controller_index] t = ctl.instance_value_type(mod) if isinstance(t, Range): value += t.min ctl.propagate(mod, value, down=True) super(MetaModule, self).on_controller_changed(controller, value, down, up) def on_embedded_controller_changed(self, module, controller, value): for i, mapping in enumerate(self.mappings.values): module_matches = mapping.module == module.index controller_matches = mapping.controller == controller.number if module_matches and controller_matches: name = self.user_defined[i].name setattr(self, name, value) def on_user_defined_controllers_changed(self, value): self.recompute_controller_attachment() def recompute_controller_attachment(self): ctl_count = self.user_defined_controllers attached_values = [True] * ctl_count + [False] * ( MAX_USER_DEFINED_CONTROLLERS - ctl_count ) for controller, attached in zip(self.user_defined, attached_values): if attached: controller.attach(self) else: controller.detach(self) def update_user_defined_controllers(self): self.mappings.update_user_defined_controllers(self) def specialized_iff_chunks(self): yield b"CHNM", pack("<I", 0) yield b"CHDT", yield from self.mappings.chunks() yield from super(MetaModule, self).specialized_iff_chunks() for i, controller in enumerate(self.user_defined, 8): if controller.attached(self) and controller.label is not None: yield b"CHNM", pack("<I", i) yield b"CHDT", controller.label.encode(rv.ENCODING) + b"\0" def load_chunk(self, chunk): if chunk.chnm == self.options_chnm: self.load_options(chunk) elif chunk.chnm == 0: self.load_project(chunk) elif chunk.chnm == 1: self.mappings.length = len(chunk.chdt) // self.mappings.element_size self.mappings.reset() self.mappings.bytes = chunk.chdt elif chunk.chnm >= 8: self.load_label(chunk) def load_project(self, chunk): self.project = read_sunvox_file(BytesIO(chunk.chdt)) def load_label(self, chunk): controller = self.user_defined[chunk.chnm - 8] data = chunk.chdt data = data[: data.find(0)] if 0 in data else data controller.label = data.decode(rv.ENCODING)