Source code for rv.modules.multictl

from __future__ import annotations

from enum import Enum
from itertools import chain

from rv.chunks import ArrayChunk
from rv.controller import CompactRange, Controller, Range
from rv.errors import MappingError
from rv.modules import Behavior as B
from rv.modules import Module
from rv.modules.base.multictl import BaseMultiCtl


def convert_value(gain, qsteps, smin, smax, dmin, dmax, vmax, value, curve=None):
    value = (value * gain) / 256
    value = min(value, 32768)
    if curve is not None:
        bucket = int(value / 128)
        start = 128 * bucket
        offset = value - start
        b = curve[bucket]
        a = curve[bucket + 1] if bucket < 256 else b
        c = min(offset / 128, 1.0)
        value = int((c * a) + ((1.0 - c) * b))
    srange = smax - smin
    if qsteps < 32768:
        quant = max(qsteps - 1, 1)
        step = 32768 / quant
        value = int(value / step)
        value = (value * step) / 32768
        value = smin + int(srange * value)
    else:
        value = smin + (srange * value) // 32768
    # TODO: out_offset
    drange = dmax - dmin
    if vmax is not None:
        value /= 32768 / vmax
    if drange > 0:
        value += dmin
    else:
        value = dmin - value
    return int(value)


def invert_value(gain, smin, smax, dmin, dmax, vmax, value):
    drange = dmax - dmin
    if drange > 0:
        value -= dmin
    else:
        value = dmin + value
    value *= drange
    # TODO: out_offset
    # TODO: map using multictl curve
    if gain == 0:
        return 0
    srange = smax - smin if smax > smin else smin - smax
    if srange == 0:
        return 0
    value *= 32768 / vmax
    value -= smin
    value /= srange
    value = min(32768, value)
    value = max(0, value)
    value *= 256
    value /= gain
    if smin >= smax:
        value = 32768 - value
    return int(value)


[docs] class MultiCtl(BaseMultiCtl, Module): mgroup = "Misc" chnk = 4 behaviors = {B.sends_controls} class Mapping: def __init__(self, value): ( self.min, self.max, self.controller, self.flags, # 1 if mapping to the same module as previous mapping self.future_use2, self.future_use3, self.future_use4, self.future_use5, ) = value[:8] class MappingArray(ArrayChunk): chnm = 0 length = 16 type = "IIIIIIII" element_size = 4 * 8 values: list[MultiCtl.Mapping] def default(self, _): return MultiCtl.Mapping((0, 0x8000, 0, 0, 0, 0, 0, 0)) @property def encoded_values(self): return list( chain.from_iterable( ( x.min, x.max, x.controller, x.flags, x.future_use2, x.future_use3, x.future_use4, x.future_use5, ) for x in self.values ) ) @property def python_type(self): return MultiCtl.Mapping def __init__(self, **kwargs): curve = kwargs.pop("curve", None) mappings = kwargs.pop("mappings", []) super(MultiCtl, self).__init__(**kwargs) self.curve = self.curve_chunk() if curve is not None: self.curve.values = curve self.mappings = self.MappingArray() for i, mapping in enumerate(mappings): self.mappings.values[i] = self.Mapping(mapping) def on_value_changed(self, value, down, up): # [TODO] this needs to be updated to handle the new flags field if self.parent is None or not down: return for i, to_mod in enumerate(self.out_links): mapping = self.mappings.values[i] mod = self.parent.modules[to_mod] ctl = list(mod.controllers.values())[mapping.controller - 1] vt = ctl.value_type if isinstance(vt, Range): vmax = None if isinstance(vt, CompactRange) else vt.max - vt.min smin, smax = mapping.min, mapping.max dmin, dmax = 0, vt.max - vt.min if smin > smax: smin, smax = smax, smin dmin, dmax = dmax, dmin converted = convert_value( self.gain, self.quantization, smin, smax, dmin, dmax, vmax, self.value, self.curve.values, ) final_value = converted + vt.min setattr(mod, ctl.name, final_value) # TODO: apply out_offset # TODO: what should we do if it's not a range? def reflect(self, index=0, propagate=True): """Reflect the value of the controller mapped at the given index. It is the inverse of setting value. """ # [TODO] this needs to be updated to handle the new flags field if index >= len(self.out_links): raise IndexError(f"No destination module mapped at index {index}") mapping = self.mappings.values[index] if mapping.controller == 0: raise IndexError(f"No destination controller mapped at index {index}") reflect_mod = self.parent.modules[self.out_links[index]] reflect_ctl_name = list(reflect_mod.controllers)[mapping.controller - 1] reflect_ctl = reflect_mod.controllers[reflect_ctl_name] reflect_value = getattr(reflect_mod, reflect_ctl_name) if hasattr(reflect_value, "value"): reflect_value = reflect_value.value t = reflect_ctl.value_type if isinstance(t, Range): dmin = t.min dmax = t.max elif t is bool: dmin = 0 dmax = 1 elif isinstance(t, type) and issubclass(t, Enum): dmin = 0 dmax = len(t) else: dmin = 0 dmax = 32768 inverted = invert_value( gain=self.gain, smin=mapping.min, smax=mapping.max, dmin=dmin, dmax=dmax, vmax=dmax - dmin if dmax > dmin else dmin - dmax, value=reflect_value, ) if propagate: self.value = inverted else: self.controller_values["value"] = inverted def specialized_iff_chunks(self): yield from self.mappings.chunks() yield from self.curve.chunks() yield from super(MultiCtl, self).specialized_iff_chunks() def load_chunk(self, chunk): if chunk.chnm == 0: self.mappings.bytes = chunk.chdt elif chunk.chnm == 1: self.curve.bytes = chunk.chdt @staticmethod def macro(project, *mod_ctl_pairs, name=None, layer=0, x=0, y=0, initial=None): if len(mod_ctl_pairs) > 16: raise MappingError("MultiCtl supports max of 16 destinations") mappings = [] mods = [] gains = set() for mod, ctl in mod_ctl_pairs: if not isinstance(ctl, Controller): ctl = mod.controllers[ctl] t = ctl.instance_value_type(mod) if isinstance(t, type) and issubclass(t, Enum): mapmin, mapmax = 0, len(t) - 1 gains.add(256 + int(256 / mapmax)) elif t is bool: mapmin, mapmax = 0, 1 gains.add(512) elif t.min == 1: mapmin, mapmax = t.min, t.max gains.add(256 + int(256 / mapmax)) elif isinstance(t, CompactRange): mapmin, mapmax = 0, (t.max - t.min) gains.add(256) else: mapmin, mapmax = 0, 0x8000 gains.add(256) mappings.append((mapmin, mapmax, ctl.number)) mods.append(project.modules[mod.index]) if len(mods) != len(set(mods)): raise MappingError( "Only one MultiCtl mapping per destination module allowed" ) gain = list(gains).pop() if gains and len(gains) == 1 else 256 bundle = project.new_module( MultiCtl, name=name, layer=layer, x=x, y=y, gain=gain, mappings=mappings ) bundle >> mods if initial is not None: bundle.value = initial return bundle