"""
Handle DMX and MIDI channel offsets
"""
[docs]class OffsetRange:
def __init__(self, offset=0, begin=None, end=None):
"""
Unlike a `range`, an OffsetRange includes both its begin *and* its end,
so it's closer to how regular people think of a range - for example
that DMX channels are in the range 1-512.
"""
self.begin = self.BEGIN if begin is None else begin
self.end = self.END if end is None else end
if not (self.BEGIN <= self.begin <= self.end <= self.END):
raise ValueError('Bad range: FAILED %s <= %s <= %s <= %s' %
(self.BEGIN, self.begin, self.end, self.END))
self.offset = offset
[docs] def full_range(self):
return self.END - self.BEGIN + 1
[docs] def index(self, i, length=None):
"""Return an integer index or None"""
if self.begin <= i <= self.end:
index = i - self.BEGIN - self.offset
if length is None:
length = self.full_range()
else:
length = min(length, self.full_range())
if 0 <= index < length:
return index
[docs] def read_from(self, data, pad=0):
"""
Returns a generator with the elements "data" taken by offset, restricted
by self.begin and self.end, and padded on either end by `pad` to get
back to the original length of `data`
"""
for i in range(self.BEGIN, self.END + 1):
index = self.index(i, len(data))
yield pad if index is None else data[index]
[docs] def copy_to(self, source, target):
if not target:
return
offset = self.offset
if offset < 0:
source = source[-offset:]
offset = 0
begin = min(self.begin - self.BEGIN, len(target) - 1)
end = min(self.end - self.BEGIN, len(target) - 1)
length = min(len(source) - offset, end - begin + 1)
target[begin:begin + length] = source[offset:offset + length]
[docs] @classmethod
def make(cls, x=0):
try:
return cls(**x)
except TypeError:
return cls(offset=x)
[docs]class MidiChannel(OffsetRange):
BEGIN = 1
END = 16
[docs]class MidiNote(OffsetRange):
BEGIN = 0
END = 127
[docs]class DMXChannel(OffsetRange):
BEGIN = 1
END = 512