Source code for rv.modules.metamodule

from __future__ import annotations

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

from slugify import slugify as _slugify

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

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


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


class UserDefined(Controller):
    label = None

    def __init__(self, number):
        self.name = f"user_defined_{number + 1}"
        self.number = number + 6
        super().__init__((0, 44100), 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 = MAX_USER_DEFINED_CONTROLLERS type = "HH" element_size = 2 * 2 values: list[MetaModule.Mapping] def default(self, _): return MetaModule.Mapping((0, 0)) def _set_bytes(self, value): super(MetaModule.MappingArray, self)._set_bytes(value) while len(self.values) < self.length: self.values.append(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 idx, (mapping, user_defined_controller) in enumerate(items): if idx == metamodule.user_defined_controllers: break if mapping.module == 0 or mapping.module >= len(project.modules): continue mod = project.modules[mapping.module] if not mod: continue controller_index = mapping.controller controller_values = list(mod.controllers.values()) if controller_index >= len(controller_values): continue controller = controller_values[controller_index] user_defined_controller.value_type = controller.instance_value_type(mod) user_defined_controller.default = controller.default metamodule.controller_values[user_defined_controller.name] = ( mod.controller_values[controller.name] ) ( 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, user_defined_28, user_defined_29, user_defined_30, user_defined_31, user_defined_32, user_defined_33, user_defined_34, user_defined_35, user_defined_36, user_defined_37, user_defined_38, user_defined_39, user_defined_40, user_defined_41, user_defined_42, user_defined_43, user_defined_44, user_defined_45, user_defined_46, user_defined_47, user_defined_48, user_defined_49, user_defined_50, user_defined_51, user_defined_52, user_defined_53, user_defined_54, user_defined_55, user_defined_56, user_defined_57, user_defined_58, user_defined_59, user_defined_60, user_defined_61, user_defined_62, user_defined_63, user_defined_64, user_defined_65, user_defined_66, user_defined_67, user_defined_68, user_defined_69, user_defined_70, user_defined_71, user_defined_72, user_defined_73, user_defined_74, user_defined_75, user_defined_76, user_defined_77, user_defined_78, user_defined_79, user_defined_80, user_defined_81, user_defined_82, user_defined_83, user_defined_84, user_defined_85, user_defined_86, user_defined_87, user_defined_88, user_defined_89, user_defined_90, user_defined_91, user_defined_92, user_defined_93, user_defined_94, user_defined_95, user_defined_96, ) = [UserDefinedProxy(__i) for __i in range(MAX_USER_DEFINED_CONTROLLERS)] def __init__(self, **kwargs): project = kwargs.get("project") self.user_defined = [ UserDefined(i) for i in range(MAX_USER_DEFINED_CONTROLLERS) ] super().__init__(**kwargs) self.mappings = self.MappingArray() self.project = project or 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 + MAX_USER_DEFINED_CONTROLLERS @property def user_defined_aliases(self): return ( [ f"u_{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 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 - 1 ) # Fix: controller.number is 1-based, mapping.controller is 0-based 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", self.project.read() 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)