mirror of
https://github.com/onyx-and-iris/vban-cmd-python.git
synced 2026-03-03 08:49:09 +00:00
289 lines
8.8 KiB
Python
289 lines
8.8 KiB
Python
from dataclasses import dataclass
|
|
from typing import NamedTuple
|
|
|
|
from vban_cmd.enums import NBS
|
|
from vban_cmd.kinds import KindMapClass
|
|
from vban_cmd.util import comp
|
|
|
|
from .headers import VbanPacket
|
|
|
|
|
|
class Levels(NamedTuple):
|
|
strip: tuple[float, ...]
|
|
bus: tuple[float, ...]
|
|
|
|
|
|
class ChannelState:
|
|
"""Represents the processed state of a single strip or bus channel"""
|
|
|
|
def __init__(self, state_bytes: bytes):
|
|
# Convert 4-byte state to integer once for efficient lookups
|
|
self._state = int.from_bytes(state_bytes, 'little')
|
|
|
|
def get_mode(self, mode_value: int) -> bool:
|
|
"""Get boolean state for a specific mode"""
|
|
return (self._state & mode_value) != 0
|
|
|
|
def get_mode_int(self, mode_value: int) -> int:
|
|
"""Get integer state for a specific mode"""
|
|
return self._state & mode_value
|
|
|
|
# Common boolean modes
|
|
@property
|
|
def mute(self) -> bool:
|
|
return (self._state & 0x00000001) != 0
|
|
|
|
@property
|
|
def solo(self) -> bool:
|
|
return (self._state & 0x00000002) != 0
|
|
|
|
@property
|
|
def mono(self) -> bool:
|
|
return (self._state & 0x00000004) != 0
|
|
|
|
@property
|
|
def mc(self) -> bool:
|
|
return (self._state & 0x00000008) != 0
|
|
|
|
# EQ modes
|
|
@property
|
|
def eq_on(self) -> bool:
|
|
return (self._state & 0x00000100) != 0
|
|
|
|
@property
|
|
def eq_ab(self) -> bool:
|
|
return (self._state & 0x00000800) != 0
|
|
|
|
# Bus assignments (strip to bus routing)
|
|
@property
|
|
def busa1(self) -> bool:
|
|
return (self._state & 0x00001000) != 0
|
|
|
|
@property
|
|
def busa2(self) -> bool:
|
|
return (self._state & 0x00002000) != 0
|
|
|
|
@property
|
|
def busa3(self) -> bool:
|
|
return (self._state & 0x00004000) != 0
|
|
|
|
@property
|
|
def busa4(self) -> bool:
|
|
return (self._state & 0x00008000) != 0
|
|
|
|
@property
|
|
def busb1(self) -> bool:
|
|
return (self._state & 0x00010000) != 0
|
|
|
|
@property
|
|
def busb2(self) -> bool:
|
|
return (self._state & 0x00020000) != 0
|
|
|
|
@property
|
|
def busb3(self) -> bool:
|
|
return (self._state & 0x00040000) != 0
|
|
|
|
|
|
class States(NamedTuple):
|
|
strip: tuple[ChannelState, ...]
|
|
bus: tuple[ChannelState, ...]
|
|
|
|
|
|
class Labels(NamedTuple):
|
|
strip: tuple[str, ...]
|
|
bus: tuple[str, ...]
|
|
|
|
|
|
@dataclass
|
|
class VbanPacketNBS0(VbanPacket):
|
|
"""Represents the body of a VBAN data packet with ident:0"""
|
|
|
|
_inputLeveldB100: bytes
|
|
_outputLeveldB100: bytes
|
|
_TransportBit: bytes
|
|
_stripState: bytes
|
|
_busState: bytes
|
|
_stripGaindB100Layer1: bytes
|
|
_stripGaindB100Layer2: bytes
|
|
_stripGaindB100Layer3: bytes
|
|
_stripGaindB100Layer4: bytes
|
|
_stripGaindB100Layer5: bytes
|
|
_stripGaindB100Layer6: bytes
|
|
_stripGaindB100Layer7: bytes
|
|
_stripGaindB100Layer8: bytes
|
|
_busGaindB100: bytes
|
|
_stripLabelUTF8c60: bytes
|
|
_busLabelUTF8c60: bytes
|
|
|
|
@classmethod
|
|
def from_bytes(cls, nbs: NBS, kind: KindMapClass, data: bytes):
|
|
return cls(
|
|
nbs=nbs,
|
|
_kind=kind,
|
|
_voicemeeterType=data[28:29],
|
|
_reserved=data[29:30],
|
|
_buffersize=data[30:32],
|
|
_voicemeeterVersion=data[32:36],
|
|
_optionBits=data[36:40],
|
|
_samplerate=data[40:44],
|
|
_inputLeveldB100=data[44:112],
|
|
_outputLeveldB100=data[112:240],
|
|
_TransportBit=data[240:244],
|
|
_stripState=data[244:276],
|
|
_busState=data[276:308],
|
|
_stripGaindB100Layer1=data[308:324],
|
|
_stripGaindB100Layer2=data[324:340],
|
|
_stripGaindB100Layer3=data[340:356],
|
|
_stripGaindB100Layer4=data[356:372],
|
|
_stripGaindB100Layer5=data[372:388],
|
|
_stripGaindB100Layer6=data[388:404],
|
|
_stripGaindB100Layer7=data[404:420],
|
|
_stripGaindB100Layer8=data[420:436],
|
|
_busGaindB100=data[436:452],
|
|
_stripLabelUTF8c60=data[452:932],
|
|
_busLabelUTF8c60=data[932:1412],
|
|
)
|
|
|
|
def pdirty(self, other) -> bool:
|
|
"""True iff any defined parameter has changed"""
|
|
|
|
self_gains = (
|
|
self._stripGaindB100Layer1
|
|
+ self._stripGaindB100Layer2
|
|
+ self._stripGaindB100Layer3
|
|
+ self._stripGaindB100Layer4
|
|
+ self._stripGaindB100Layer5
|
|
+ self._stripGaindB100Layer6
|
|
+ self._stripGaindB100Layer7
|
|
+ self._stripGaindB100Layer8
|
|
)
|
|
other_gains = (
|
|
other._stripGaindB100Layer1
|
|
+ other._stripGaindB100Layer2
|
|
+ other._stripGaindB100Layer3
|
|
+ other._stripGaindB100Layer4
|
|
+ other._stripGaindB100Layer5
|
|
+ other._stripGaindB100Layer6
|
|
+ other._stripGaindB100Layer7
|
|
+ other._stripGaindB100Layer8
|
|
)
|
|
|
|
return (
|
|
self._stripState != other._stripState
|
|
or self._busState != other._busState
|
|
or self_gains != other_gains
|
|
or self._busGaindB100 != other._busGaindB100
|
|
or self._stripLabelUTF8c60 != other._stripLabelUTF8c60
|
|
or self._busLabelUTF8c60 != other._busLabelUTF8c60
|
|
)
|
|
|
|
def ldirty(self, strip_cache, bus_cache) -> bool:
|
|
"""True iff any level has changed, ignoring changes when levels are very quiet"""
|
|
self._strip_comp, self._bus_comp = (
|
|
tuple(not val for val in comp(strip_cache, self.strip_levels)),
|
|
tuple(not val for val in comp(bus_cache, self.bus_levels)),
|
|
)
|
|
return any(self._strip_comp) or any(self._bus_comp)
|
|
|
|
@property
|
|
def strip_levels(self) -> tuple[float, ...]:
|
|
"""Returns strip levels in dB"""
|
|
return tuple(
|
|
round(
|
|
int.from_bytes(self._inputLeveldB100[i : i + 2], 'little', signed=True)
|
|
* 0.01,
|
|
1,
|
|
)
|
|
for i in range(0, len(self._inputLeveldB100), 2)
|
|
)[: self._kind.num_strip_levels]
|
|
|
|
@property
|
|
def bus_levels(self) -> tuple[float, ...]:
|
|
"""Returns bus levels in dB"""
|
|
return tuple(
|
|
round(
|
|
int.from_bytes(self._outputLeveldB100[i : i + 2], 'little', signed=True)
|
|
* 0.01,
|
|
1,
|
|
)
|
|
for i in range(0, len(self._outputLeveldB100), 2)
|
|
)[: self._kind.num_bus_levels]
|
|
|
|
@property
|
|
def levels(self) -> Levels:
|
|
"""Returns strip and bus levels as a namedtuple"""
|
|
return Levels(strip=self.strip_levels, bus=self.bus_levels)
|
|
|
|
@property
|
|
def states(self) -> States:
|
|
"""returns States object with processed strip and bus channel states"""
|
|
return States(
|
|
strip=tuple(
|
|
ChannelState(self._stripState[i : i + 4]) for i in range(0, 32, 4)
|
|
),
|
|
bus=tuple(ChannelState(self._busState[i : i + 4]) for i in range(0, 32, 4)),
|
|
)
|
|
|
|
@property
|
|
def gainlayers(self) -> tuple:
|
|
"""returns tuple of all strip gain layers as tuples"""
|
|
return tuple(
|
|
tuple(
|
|
round(
|
|
int.from_bytes(
|
|
getattr(self, f'_stripGaindB100Layer{layer}')[i : i + 2],
|
|
'little',
|
|
signed=True,
|
|
)
|
|
* 0.01,
|
|
2,
|
|
)
|
|
for i in range(0, 16, 2)
|
|
)
|
|
for layer in range(1, 9)
|
|
)
|
|
|
|
@property
|
|
def busgain(self) -> tuple:
|
|
"""returns tuple of bus gains"""
|
|
return tuple(
|
|
round(
|
|
int.from_bytes(self._busGaindB100[i : i + 2], 'little', signed=True)
|
|
* 0.01,
|
|
2,
|
|
)
|
|
for i in range(0, 16, 2)
|
|
)
|
|
|
|
@property
|
|
def labels(self) -> Labels:
|
|
"""returns Labels namedtuple of strip and bus labels"""
|
|
|
|
def _extract_labels_from_bytes(label_bytes: bytes) -> tuple[str, ...]:
|
|
"""Extract null-terminated UTF-8 labels from 60-byte chunks"""
|
|
labels = []
|
|
for i in range(0, len(label_bytes), 60):
|
|
chunk = label_bytes[i : i + 60]
|
|
null_pos = chunk.find(b'\x00')
|
|
if null_pos == -1:
|
|
try:
|
|
label = chunk.decode('utf-8', errors='replace').rstrip('\x00')
|
|
except UnicodeDecodeError:
|
|
label = ''
|
|
else:
|
|
try:
|
|
label = (
|
|
chunk[:null_pos].decode('utf-8', errors='replace')
|
|
if null_pos > 0
|
|
else ''
|
|
)
|
|
except UnicodeDecodeError:
|
|
label = ''
|
|
labels.append(label)
|
|
return tuple(labels)
|
|
|
|
return Labels(
|
|
strip=_extract_labels_from_bytes(self._stripLabelUTF8c60),
|
|
bus=_extract_labels_from_bytes(self._busLabelUTF8c60),
|
|
)
|