babymix/main.py

460 lines
14 KiB
Python

import math
from rich.console import ConsoleOptions, Console
from rich.segment import Segment
from rich.style import Style
from textual.app import App, ComposeResult, Widget, RenderResult
from textual.color import Color
from textual.containers import HorizontalScroll, Horizontal, Vertical, Container
from textual.reactive import reactive, var
from textual.widgets import Static, Button, Pretty, Log, Footer, Header
import mido
from encoding import decode_message, encode_message, hex_format, get_bitfield, level_to_db, format_db
import encoding
class MixerMatrix:
def __init__(self, out_port: mido.ports.BaseOutput, in_channels = 10, play_channels = 12, out_channels = 12) -> None:
self.out_port = out_port
self.in_channels = in_channels
self.in_levels = [[-120.0 for _ in range(in_channels)] for _ in range(out_channels)]
self.play_levels = [[0.0 if p == o else -120.0 for p in range(play_channels)] for o in range(out_channels)]
self.out_levels = [0.0 for _ in range(out_channels)]
def update_out(self, channel, val):
self.out_levels[channel] = val
def update_in(self, in_channel, out_channel, val):
self.in_levels[out_channel][in_channel] = val
self._send_update(in_channel, out_channel, val)
def update_play(self, play_channel, out_channel, val):
self.play_levels[out_channel][play_channel] = val
self._send_update(play_channel + self.in_channels, out_channel, val)
def _send_out_update(self, out_channel, val):
reg = 0x3e0 | out_channel
msg = val << 15 | reg
self.out_port.send(encode_message(encoding.SUBID_MIXER, msg))
if out_channel < 4:
gain_val = int(0x3b + (val / 2**16) * 0x3b)
gain_msg = gain_val << 16 | 0x10 + out_channel
self.out_port.send(encode_message(encoding.SUBID_GAIN, gain_msg))
def _send_update(self, in_channel, out_channel, val):
reg = out_channel * 24 + in_channel
msg = val << 15 | reg
self.out_port.send(encode_message(encoding.SUBID_MIXER, msg))
class MeterGradient:
FRACTIONS = [
' ',
'',
'',
'',
'',
'',
'',
'',
'',
]
def __init__(self, value: int, color1: str, color2: str) -> None:
self._color1 = Color.parse(color1)
self._color2 = Color.parse(color2)
self._value = 1 - value
def __rich_console__(
self, console: Console, options: ConsoleOptions
) -> RenderResult:
width = options.max_width
height = options.height or options.max_height
color1 = self._color1
color2 = self._color2
from_color = Style.from_color
blend = color1.blend
rich_color1 = color1.rich_color
top_char = self.FRACTIONS[math.floor((self._value * height * 8) % 8)]
top = math.ceil(self._value * height)
for y in range(height):
rel_y = y / (height - 1)
if y < top:
yield Segment(f"{width * ' '}\n")
else:
line_color = from_color(
(
blend(color2, rel_y).rich_color
if height > 1
else rich_color1
),
None,
)
char = top_char if y == top else ''
yield Segment(f"{width * char}\n", line_color)
class Meter(Widget):
DEFAULT_CSS = """
Meter {
width: 1
}
"""
value = reactive(1.0)
def render(self) -> RenderResult:
return MeterGradient(self.value, '#04ff00', '#04aa00')
class Channel(Widget):
DEFAULT_CSS = """
Channel {
width: 11;
border: solid #333333;
Horizontal {
height: 100%;
}
.label {
width: 100%;
text-align: center;
}
.controls {
margin-left: 1;
margin-right: 1;
width: 5
}
Button {
margin-bottom: 1;
padding: 0;
min-width: 6;
height: 1;
}
.mute {
background: #00f !important;
}
.solo {
background: #0ff;
}
.phantom {
background: #f00;
}
.inst {
background: #ff0;
}
.indicators-wrap {
margin-left: 1;
align: left bottom;
}
.indicators {
height: 4;
}
.peak, .rms {
color: #55aa00;
}
.level {
text-style: bold;
}
}
"""
rms_value = var(0)
peak_value = var(0)
level_value = var(0x20000000)
db_rms_value = reactive(-100.0)
db_peak_value = reactive(-100.0)
db_level_value = reactive(0.0)
RMS_FACTOR = float(2**54)
PEAK_FACTOR = float(2**23)
def compute_db_level_value(self) -> float:
return level_to_db(self.level_value)
def compute_db_peak_value(self) -> float:
try:
return 20 * math.log10((self.peak_value >> 4) / self.PEAK_FACTOR)
except ValueError:
return -120
def compute_db_rms_value(self) -> float:
try:
return 10 * math.log10(self.rms_value / self.RMS_FACTOR)
except ValueError:
return -120
def watch_db_level_value(self, val) -> None:
self.query_one('.level').update(format_db(val))
def watch_db_peak_value(self, val) -> None:
self.query_one('.peak').update(format_db(val))
def watch_db_rms_value(self, val) -> None:
self.query_one('.rms').update(format_db(val))
self.query_one(Meter).value = (val + 90) / 110
def __init__(self, label, phantom=False, inst=False, *children: Widget):
super().__init__(*children)
self.label = label
self.phantom = phantom
self.inst = inst
def compose(self) -> ComposeResult:
yield Static(self.label, classes='label')
with Horizontal():
yield Meter()
with Vertical():
with Vertical(classes='controls'):
yield Button("M", classes='mute', compact=True)
yield Button("S", classes='solo', compact=True)
if self.phantom:
yield Button("48v", classes='phantom', compact=True)
if self.inst:
yield Button("Inst", classes='inst', compact=True)
with Vertical(classes='indicators-wrap'):
with Container(classes='indicators'):
yield Static('0.0', classes='level')
yield Static('0.0', classes='peak')
yield Static('0.1', classes='rms')
class ChannelRow(Widget):
DEFAULT_CSS = """
ChannelRow {
height: 33%
}
"""
rms_values = var([])
peak_values = var([])
level_values = var([])
def watch_rms_values(self, new_values):
for channel, value in zip(self.query(Channel), new_values):
channel.rms_value = value
def watch_peak_values(self, new_values):
for channel, value in zip(self.query(Channel), new_values):
channel.peak_value = value
def watch_level_values(self, new_values):
for channel, value in zip(self.query(Channel), new_values):
channel.level_value = value
class InputChannelRow(ChannelRow):
def compose(self) -> ComposeResult:
with HorizontalScroll():
yield Channel('AN1', phantom=True)
yield Channel('AN2', phantom=True, inst=True)
yield Channel('ADAT1')
yield Channel('ADAT2')
yield Channel('ADAT3')
yield Channel('ADAT4')
yield Channel('ADAT5')
yield Channel('ADAT6')
yield Channel('ADAT7')
yield Channel('ADAT8')
class PlaybackChannelRow(ChannelRow):
def compose(self) -> ComposeResult:
with HorizontalScroll():
yield Channel('AN1')
yield Channel('AN2')
yield Channel('PH3')
yield Channel('PH4')
yield Channel('ADAT1')
yield Channel('ADAT2')
yield Channel('ADAT3')
yield Channel('ADAT4')
yield Channel('ADAT5')
yield Channel('ADAT6')
yield Channel('ADAT7')
yield Channel('ADAT8')
class OutputChannelRow(ChannelRow):
def compose(self) -> ComposeResult:
with HorizontalScroll():
yield Channel('AN1')
yield Channel('AN2')
yield Channel('PH3')
yield Channel('PH4')
yield Channel('ADAT1')
yield Channel('ADAT2')
yield Channel('ADAT3')
yield Channel('ADAT4')
yield Channel('ADAT5')
yield Channel('ADAT6')
yield Channel('ADAT7')
yield Channel('ADAT8')
class BFStatus:
word0 = 0
word1 = 0
word2 = 0
# word 0
ch1_48v = 0
ch2_48v = 0
ch2_inst = 0
digital_out = 0 # 0 = ADAT, 1 = SPDIF
# word 1
out_1_vol = 0
out_2_vol = 0
ch1_gain = 0
ch2_gain = 0
# word 2
out_3_vol = 0
out_4_vol = 0
dim = 0
def parse(self, msg):
self.word0 = format(msg[0], '032b')
if len(msg) > 2:
self.word1 = format(msg[1], '032b')
self.word2 = format(msg[2], '032b')
self.ch1_48v = get_bitfield(msg[0], 31, 1)
self.ch2_48v = get_bitfield(msg[0], 30, 1)
self.ch2_inst = get_bitfield(msg[0], 29, 1)
self.digital_out = get_bitfield(msg[0], 28, 1)
self.out_1_vol = level_to_db(get_bitfield(msg[1], 0, 8))
self.out_2_vol = level_to_db(get_bitfield(msg[1], 9, 8))
self.ch2_gain = get_bitfield(msg[1], 24, 5)
self.ch1_gain = get_bitfield(msg[1], 19, 5)
self.out_3_vol = level_to_db(get_bitfield(msg[2], 15, 8))
self.out_4_vol = level_to_db(get_bitfield(msg[2], 23, 8))
self.dim = get_bitfield(msg[2], 5, 1)
class BFRMS:
def __init__(self):
self.input_rms = [0 for _ in range(10)]
self.playback_rms = [0 for _ in range(12)]
self.fx_in_rms = [0, 0]
self.output_rms = [0 for _ in range(12)]
self.fx_out_rms = [0, 0]
def _parse_rms(self, values, target, offset=0):
n = offset
for i in range(0, len(values), 2):
target[n] = (values[i] | (values[i + 1] << 32))
n += 1
def parse0(self, id0_msg):
self._parse_rms(id0_msg[3:23], self.input_rms)
self._parse_rms(id0_msg[23:], self.playback_rms)
def parse1(self, id1_msg):
self._parse_rms(id1_msg[:6], self.playback_rms, 9)
self._parse_rms(id1_msg[6:10], self.fx_in_rms)
self._parse_rms(id1_msg[10:34], self.output_rms)
self._parse_rms(id1_msg[34:38], self.fx_in_rms)
class BFPeak:
def __init__(self):
self.input_peaks = [0 for _ in range(10)]
self.playback_peaks = [0 for _ in range(12)]
self.fx_in_peaks = [0, 0]
self.output_peaks = [0 for _ in range(12)]
self.fx_out_peaks = [0, 0]
def parse(self, msg):
self.input_peaks = msg[:10]
self.playback_peaks = msg[10:22]
self.fx_in_peaks = msg[22:24]
self.output_peaks = msg[24:36]
self.fx_out_peaks = msg[36:38]
class Status(Widget):
status_data = reactive({})
def compose(self) -> ComposeResult:
yield Pretty(self.status_data, id='status')
yield Pretty(self.status_data, id='rms')
yield Pretty(self.status_data, id='peak')
yield Log(self.status_data, id='log')
class Mixer(App):
in_port: mido.ports.BaseInput
out_port: mido.ports.BaseInput
messages = []
messageset = set()
STATUS_MSG = mido.Message('sysex', data=[0x00, 0x20, 0x0D, 0x10, 0x10])
bf_status = BFStatus()
bf_rms = BFRMS()
bf_peak = BFPeak()
def __init__(self):
super().__init__()
in_ports = [name for name in mido.get_input_names() if 'Babyface' in name and 'Port 2' in name]
out_ports = [name for name in mido.get_output_names() if 'Babyface' in name and 'Port 2' in name]
if in_ports:
self.in_port = mido.open_input(in_ports[0])
else:
self.in_port = mido.open_input('Mixer', virtual=True)
if out_ports:
self.out_port = mido.open_output(out_ports[0])
else:
self.out_port = mido.open_output('Mixer', virtual=True)
def on_mount(self) -> None:
self.set_interval(.1, self.update_messages)
def update_messages(self) -> None:
self.out_port.send(self.STATUS_MSG)
for msg in self.in_port.iter_pending():
self.process_message(msg)
def compose(self) -> ComposeResult:
yield Header()
yield InputChannelRow()
yield PlaybackChannelRow()
yield OutputChannelRow()
yield Footer()
def process_message(self, msg: mido.Message) -> None:
subid, decoded_msg = decode_message(msg)
if subid is None:
return
elif subid == 0x00:
self.bf_status.parse(decoded_msg)
self.bf_rms.parse0(decoded_msg)
elif subid == 0x01:
self.bf_rms.parse1(decoded_msg)
elif subid == 0x02:
self.bf_peak.parse(decoded_msg)
else:
self.messages.append(hex_format(decoded_msg))
self.query_one('#log').update(self.messages)
return
input_row = self.query_one(InputChannelRow)
input_row.rms_values = self.bf_rms.input_rms
input_row.peak_values = self.bf_peak.input_peaks
input_row.mutate_reactive(ChannelRow.rms_values)
input_row.mutate_reactive(ChannelRow.peak_values)
playback_row = self.query_one(PlaybackChannelRow)
playback_row.rms_values = self.bf_rms.playback_rms
playback_row.peak_values = self.bf_peak.playback_peaks
playback_row.mutate_reactive(ChannelRow.rms_values)
playback_row.mutate_reactive(ChannelRow.peak_values)
output_row = self.query_one(OutputChannelRow)
output_row.rms_values = self.bf_rms.output_rms
output_row.peak_values = self.bf_peak.output_peaks
output_row.mutate_reactive(ChannelRow.rms_values)
output_row.mutate_reactive(ChannelRow.peak_values)
#self.query_one('#status').update(self.bf_status.__dict__)
if __name__ == '__main__':
app = Mixer()
app.run()