from enum import Enum
from itertools import chain
from rv.chunks import ArrayChunk
from rv.controller import Controller, Range, CompactRange
from rv.errors import MappingError
from rv.modules import Behavior as B, Module
def convert_value(gain, qsteps, smin, smax, dmin, dmax, vmax, value):
# TODO: map using multictl curve
value = (value * gain) / 256
value = min(value, 32768)
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(Module):
name = mtype = 'MultiCtl'
mgroup = 'Misc'
chnk = 0x10
behaviors = {B.sends_controls}
class Mapping(object):
def __init__(self, value):
self.min, self.max, self.controller = value[:3]
class MappingArray(ArrayChunk):
chnm = 0
length = 16
type = 'IIIIIIII'
element_size = 4 * 8
def default(self, _):
return MultiCtl.Mapping((0, 0x8000, 0))
@property
def encoded_values(self):
return list(chain.from_iterable(
(x.min, x.max, x.controller, 0, 0, 0, 0, 0)
for x in self.values))
@property
def python_type(self):
return MultiCtl.Mapping
class Curve(ArrayChunk):
chnm = 1
length = 257
type = 'H'
element_size = 2
min_value = 0
max_value = 0x8000
def default(self, x):
return x * 0x80
value = Controller((0, 32768), 0)
gain = Controller((0, 1024), 256)
quantization = Controller((0, 32768), 32768)
out_offset = Controller((-16384, 16384), 0)
def __init__(self, **kwargs):
curve = kwargs.pop('curve', None)
mappings = kwargs.pop('mappings', [])
super(MultiCtl, self).__init__(**kwargs)
self.curve = self.Curve()
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):
if self.parent is not None and down:
downstream_mods = []
for to_mod in range(256):
from_mods = self.parent.module_connections[to_mod]
if self.index in from_mods:
downstream_mods.append(to_mod)
for i, to_mod in enumerate(downstream_mods):
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):
if isinstance(vt, CompactRange):
vmax = None
else:
vmax = 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,
)
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; inverse of setting value"""
downstream_mods = []
for to_mod in range(256):
from_mods = self.parent.module_connections[to_mod]
if self.index in from_mods:
downstream_mods.append(to_mod)
if len(downstream_mods) == index + 1:
break
else:
raise IndexError('No destination module mapped at index {}'.format(index))
mapping = self.mappings.values[index]
if mapping.controller == 0:
raise IndexError('No destination controller mapped at index {}'.format(index))
reflect_mod = self.parent.modules[downstream_mods[-1]]
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):
for chunk in self.mappings.chunks():
yield chunk
for chunk in self.curve.chunks():
yield chunk
for chunk in super(MultiCtl, self).specialized_iff_chunks():
yield chunk
def load_chunk(self, chunk):
if chunk.chnm == 0:
self.mappings.bytes = chunk.chdt
if 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.value_type
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')
if gains and len(gains) == 1:
gain = list(gains).pop()
else:
gain = 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