Added basic functionality

- gain
- mute
- mixer
- channel setup options
- keybinds and mousebinds
- focus behavior
This commit is contained in:
Brian Hrebec 2026-03-12 23:33:13 -05:00
parent f444daeeca
commit 04060ef5a1
2 changed files with 491 additions and 108 deletions

View file

@ -63,12 +63,21 @@ def set_bitfield(word, val, position, length):
return (word & mask << position) | val return (word & mask << position) | val
"""
Special level calculation for main/ph outputs
"""
def level_to_db(val): def level_to_db(val):
return 6 + (val - 0xFF) / 2 return 6 + (val - 0xFF) / 2
"""
Special level calculation for main/ph outputs
"""
def db_to_level(val):
return int((val - 6) * 2) + 0xFF
def fader_to_db(val):
return 6 + (abs(val) - 0xFF) / 2 def db_to_fader(db):
return int(10 ** (db / 20) * 0x40000)
def format_db(val, min_db=-120): def format_db(val, min_db=-120):
@ -76,3 +85,9 @@ def format_db(val, min_db=-120):
return '' return ''
else: else:
return format(val, '.1f') return format(val, '.1f')
def map_range(val, min_, max_, low, high):
return ((clamp(val, min_, max_) - min_) / (max_ - min_)) * (high - low) + low
def clamp(val, low, high):
return max(min(val, high), low)

580
main.py
View file

@ -1,51 +1,113 @@
import math import math
from collections import namedtuple
from rich.console import ConsoleOptions, Console from rich.console import ConsoleOptions, Console
from rich.segment import Segment from rich.segment import Segment
from rich.style import Style from rich.style import Style
from textual import events
from textual.app import App, ComposeResult, Widget, RenderResult from textual.app import App, ComposeResult, Widget, RenderResult
from textual.color import Color from textual.color import Color
from textual.containers import HorizontalScroll, Horizontal, Vertical, Container from textual.containers import HorizontalScroll, Horizontal, Vertical, Container
from textual.events import Event, MouseEvent
from textual.geometry import Size
from textual.reactive import reactive, var from textual.reactive import reactive, var
from textual.strip import Strip
from textual.widgets import Static, Button, Pretty, Log, Footer, Header from textual.widgets import Static, Button, Pretty, Log, Footer, Header
from textual.messages import Message
import mido import mido
from encoding import decode_message, encode_message, hex_format, get_bitfield, level_to_db, format_db from encoding import decode_message, encode_message, hex_format, get_bitfield, level_to_db, format_db, clamp
import encoding import encoding
ChannelState = namedtuple('ChannelState', ['level', 'muted', 'soloed'])
class MixerMatrix: class MixerMatrix:
def __init__(self, out_port: mido.ports.BaseOutput, in_channels = 10, play_channels = 12, out_channels = 12) -> None: def __init__(self, out_port: mido.ports.BaseOutput, in_channels=10, play_channels=12, out_channels=12) -> None:
self.out_port = out_port self.out_port = out_port
self.in_channels = in_channels self.matrix = [[ -120.0 for _ in range(out_channels) ] for _ in range(in_channels)] + [
self.in_levels = [[-120.0 for _ in range(in_channels)] for _ in range(out_channels)] [ 0.0 if p == o else -120.0 for p in range(play_channels)
self.play_levels = [[0.0 if p == o else -120.0 for p in range(play_channels)] for o in range(out_channels)] ] for o in range(out_channels)]
self.out_levels = [0.0 for _ in range(out_channels)]
def update_out(self, channel, val): self.outputs = [0.0 for _ in range(out_channels)]
self.out_levels[channel] = val self.output_mutes = [False for _ in range(out_channels)]
self.output_solos = [False for _ in range(out_channels)]
self.input_mutes = [False for _ in range(in_channels + play_channels)]
self.input_solos = [False for _ in range(in_channels + play_channels)]
def update_in(self, in_channel, out_channel, val): def toggle_out_solo_status(self, channel):
self.in_levels[out_channel][in_channel] = val solo = self.output_solos[channel] = not self.output_solos[channel]
self._send_update(in_channel, out_channel, val) if solo or any(self.output_solos):
for channel, channel_solo in enumerate(self.output_solos):
if channel_solo:
self._send_out_update(channel, self.outputs[channel])
else:
self._send_out_update(channel, -120)
else:
for channel, level in enumerate(self.outputs):
if not self.output_mutes[channel]:
self._send_out_update(channel, level)
def update_play(self, play_channel, out_channel, val): def toggle_out_mute_status(self, channel):
self.play_levels[out_channel][play_channel] = val mute = self.output_mutes[channel] = not self.output_mutes[channel]
self._send_update(play_channel + self.in_channels, out_channel, val) disabled = self.outputs[channel] == -120.0
soloed = self.output_solos[channel]
if mute and not disabled and not soloed:
self._send_out_update(channel, -120)
elif not mute and not disabled:
self._send_out_update(channel, self.outputs[channel])
def _send_out_update(self, out_channel, val): def set_out_level(self, channel, val):
self.outputs[channel] = clamp(val, -120, 6)
if not self.output_mutes[channel]:
self._send_out_update(channel, self.outputs[channel])
def toggle_mix_mute_status(self, in_channel):
mute = self.input_mutes[in_channel] = not self.input_mutes[in_channel]
for out_channel, level in enumerate(self.matrix[in_channel]):
if mute and level != -120:
self._send_mix_update(in_channel, out_channel, -120)
else:
self._send_mix_update(in_channel, out_channel, level)
def set_mix_level(self, in_channel, out_channel, val):
self.matrix[in_channel][out_channel] = clamp(val, -120, 6)
if not self.input_mutes[in_channel]:
self._send_mix_update(in_channel, out_channel, val)
def _send_out_update(self, out_channel, db_val):
reg = 0x3e0 | out_channel reg = 0x3e0 | out_channel
msg = val << 15 | reg msg = encoding.db_to_fader(db_val) << 12 | 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): self.out_port.send(encode_message(encoding.SUBID_MIXER, [msg]))
if out_channel < 4:
gain_val = encoding.db_to_level(clamp(db_val, -92, 6))
gain_msg = gain_val << 16 | 0x2 + out_channel
self.out_port.send(encode_message(encoding.SUBID_GAIN, [gain_msg]))
def _send_mix_update(self, in_channel, out_channel, db_val):
reg = out_channel * 24 + in_channel reg = out_channel * 24 + in_channel
msg = val << 15 | reg msg = encoding.db_to_fader(db_val) << 12 | reg
self.out_port.send(encode_message(encoding.SUBID_MIXER, msg)) self.out_port.send(encode_message(encoding.SUBID_MIXER, [msg]))
class FaderIndicator(Widget):
DEFAULT_CSS = """
FaderIndicator {
width: 1;
}
"""
value = reactive(0.925)
def render_line(self, y: int):
if y == math.floor((self.region.height - 1) * (1 - self.value)):
c = '<'
else:
c = ' '
return Strip((Segment(c),), 1)
def get_content_width(self, container: Size, viewport: Size) -> int:
return 1
class MeterGradient: class MeterGradient:
FRACTIONS = [ FRACTIONS = [
@ -65,7 +127,6 @@ class MeterGradient:
self._color2 = Color.parse(color2) self._color2 = Color.parse(color2)
self._value = 1 - value self._value = 1 - value
def __rich_console__( def __rich_console__(
self, console: Console, options: ConsoleOptions self, console: Console, options: ConsoleOptions
) -> RenderResult: ) -> RenderResult:
@ -102,6 +163,7 @@ class Meter(Widget):
} }
""" """
value = reactive(1.0) value = reactive(1.0)
def render(self) -> RenderResult: def render(self) -> RenderResult:
return MeterGradient(self.value, '#04ff00', '#04aa00') return MeterGradient(self.value, '#04ff00', '#04aa00')
@ -109,7 +171,7 @@ class Meter(Widget):
class Channel(Widget): class Channel(Widget):
DEFAULT_CSS = """ DEFAULT_CSS = """
Channel { Channel {
width: 11; width: 12;
border: solid #333333; border: solid #333333;
Horizontal { Horizontal {
height: 100%; height: 100%;
@ -129,17 +191,17 @@ class Channel(Widget):
min-width: 6; min-width: 6;
height: 1; height: 1;
} }
.mute { .mute.enabled {
background: #00f !important; background: #00f !important;
} }
.solo { .solo.enabled {
background: #0ff; background: #0ff !important;
} }
.phantom { .phantom.enabled {
background: #f00; background: #f00 !important;
} }
.inst { .inst.enabled {
background: #ff0; background: #ff0 !important;
} }
.indicators-wrap { .indicators-wrap {
margin-left: 1; margin-left: 1;
@ -155,19 +217,85 @@ class Channel(Widget):
text-style: bold; text-style: bold;
} }
} }
Channel.selected {
border: #fff;
}
Channel:focus {
background: #222;
border: #fff;
}
""" """
BINDINGS = [
('equals', 'move_fader(.5)'),
('plus', 'move_fader(.5)'),
('minus', 'move_fader(-.5)'),
('m', 'mute'),
('s', 'solo'),
('p', 'phantom'),
('i', 'inst'),
('q', 'gain(-3)'),
('e', 'gain(3)'),
]
can_focus = True
selected = reactive(False)
mute = reactive(False)
solo = reactive(False)
phantom = reactive(False)
inst = reactive(False)
gain = reactive(0.0)
level = reactive(0.0)
rms_value = var(0) rms_value = var(0)
peak_value = var(0) peak_value = var(0)
level_value = var(0x20000000)
db_rms_value = reactive(-100.0) db_rms_value = reactive(-100.0)
db_peak_value = reactive(-100.0) db_peak_value = reactive(-100.0)
db_level_value = reactive(0.0) RMS_FACTOR = float(2 ** 54)
RMS_FACTOR = float(2**54) PEAK_FACTOR = float(2 ** 23)
PEAK_FACTOR = float(2**23)
def compute_db_level_value(self) -> float: class MoveFader(Message):
return level_to_db(self.level_value) def __init__(self, channel: int, output=False, change=0):
self.channel = channel
self.output = output
self.change = change
super().__init__()
class ChangeGain(MoveFader):
pass
class ChannelMessage(Message):
def __init__(self, channel: int, output=False):
self.channel = channel
self.output = output
super().__init__()
class MuteToggle(ChannelMessage):
pass
class SoloToggle(ChannelMessage):
pass
class PhantomToggle(ChannelMessage):
pass
class InstToggle(ChannelMessage):
pass
class LevelSet(Message):
def __init__(self, channel: int, val: int, output=False):
self.channel = channel
self.val = val
self.output = output
super().__init__()
def __init__(self, channel: int, label: str, phantom=False, inst=False, output=False, *children: Widget):
super().__init__(*children)
self.channel = channel
self.label = label
self.phantom_available = phantom
self.inst_available = inst
self.output = output
def compute_db_peak_value(self) -> float: def compute_db_peak_value(self) -> float:
try: try:
@ -181,41 +309,128 @@ class Channel(Widget):
except ValueError: except ValueError:
return -120 return -120
def watch_db_level_value(self, val) -> None: def watch_level(self, val) -> None:
self.query_one('.level').update(format_db(val)) self.query_one('.level').update(format_db(val))
self.query_one('#level-indicator').value = encoding.map_range(val, -75, 6, 0, 1)
def watch_gain(self, val) -> None:
if not self.phantom_available:
return
self.query_one('#gain').update(format_db(val))
def watch_mute(self, val) -> None:
if val:
self.query_one('#mute').add_class('enabled')
else:
self.query_one('#mute').remove_class('enabled')
def watch_solo(self, val) -> None:
if val:
self.query_one('#solo').add_class('enabled')
else:
self.query_one('#solo').remove_class('enabled')
def watch_phantom(self, val) -> None:
if not self.phantom_available:
return
if val:
self.query_one('#phantom').add_class('enabled')
else:
self.query_one('#phantom').remove_class('enabled')
def watch_inst(self, val) -> None:
if not self.inst_available:
return
if val:
self.query_one('#inst').add_class('enabled')
else:
self.query_one('#inst').remove_class('enabled')
def watch_selected(self, val) -> None:
if val:
self.add_class('selected')
else:
self.remove_class('selected')
def watch_db_peak_value(self, val) -> None: def watch_db_peak_value(self, val) -> None:
self.query_one('.peak').update(format_db(val)) self.query_one('.peak').update(format_db(val))
def watch_db_rms_value(self, val) -> None: def watch_db_rms_value(self, val) -> None:
self.query_one('.rms').update(format_db(val)) self.query_one('.rms').update(format_db(val))
self.query_one(Meter).value = (val + 90) / 110 self.query_one(Meter).value = encoding.map_range(val, -75, 6, 0, 1)
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: def compose(self) -> ComposeResult:
yield Static(self.label, classes='label') yield Static(self.label, classes='label')
with Horizontal(): with Horizontal():
yield Meter() yield Meter(id='meter')
yield FaderIndicator(id='level-indicator')
with Vertical(): with Vertical():
with Vertical(classes='controls'): with Vertical(classes='controls'):
yield Button("M", classes='mute', compact=True) yield Button("M", classes='mute', compact=True, id='mute')
yield Button("S", classes='solo', compact=True) yield Button("S", classes='solo', compact=True, id='solo')
if self.phantom: if self.phantom_available:
yield Button("48v", classes='phantom', compact=True) yield Button("48v", classes='phantom', compact=True, id='phantom')
if self.inst: if self.inst_available:
yield Button("Inst", classes='inst', compact=True) yield Button("Inst", classes='inst', compact=True, id='inst')
if self.phantom_available:
yield Static('0.0', id='gain')
with Vertical(classes='indicators-wrap'): with Vertical(classes='indicators-wrap'):
with Container(classes='indicators'): with Container(classes='indicators'):
yield Static('0.0', classes='level') yield Static('0.0', classes='level')
yield Static('0.0', classes='peak') yield Static('0.0', classes='peak')
yield Static('0.1', classes='rms') yield Static('0.1', classes='rms')
def action_mute(self) -> None:
self.post_message(self.MuteToggle(self.channel, output=self.output))
def action_solo(self) -> None:
self.post_message(self.SoloToggle(self.channel, output=self.output))
def action_phantom(self) -> None:
if not self.phantom_available:
return
self.post_message(self.PhantomToggle(self.channel, output=self.output))
def action_inst(self) -> None:
if not self.inst_available:
return
self.post_message(self.InstToggle(self.channel, output=self.output))
def action_move_fader(self, change) -> None:
self.post_message(self.MoveFader(self.channel, output=self.output, change=change))
def action_gain(self, change) -> None:
self.post_message(self.ChangeGain(self.channel, output=self.output, change=change))
def _on_mouse_scroll_up(self, event: events.MouseScrollUp) -> None:
if event.shift and self.phantom_available:
self.action_gain(3)
else:
self.action_move_fader(1)
def _on_mouse_scroll_down(self, event: events.MouseScrollUp) -> None:
if event.shift and self.phantom_available:
self.action_gain(-3)
else:
self.action_move_fader(-1)
def on_button_pressed(self, event):
if 'mute' == event.button.id:
self.action_mute()
elif 'solo' == event.button.id:
self.action_solo()
elif 'phantom' == event.button.id:
self.action_phantom()
elif 'inst' == event.button.id:
self.action_inst()
class ChannelRow(Widget): class ChannelRow(Widget):
DEFAULT_CSS = """ DEFAULT_CSS = """
@ -228,6 +443,10 @@ class ChannelRow(Widget):
peak_values = var([]) peak_values = var([])
level_values = var([]) level_values = var([])
def __init__(self, channels: range, *children: Widget):
super().__init__(*children)
self.channels = channels
def watch_rms_values(self, new_values): def watch_rms_values(self, new_values):
for channel, value in zip(self.query(Channel), new_values): for channel, value in zip(self.query(Channel), new_values):
channel.rms_value = value channel.rms_value = value
@ -240,52 +459,93 @@ class ChannelRow(Widget):
for channel, value in zip(self.query(Channel), new_values): for channel, value in zip(self.query(Channel), new_values):
channel.level_value = value channel.level_value = value
def update_mixer_display(self, mixer: MixerMatrix, current_out: int):
pass
class InputChannelRow(ChannelRow): class InputChannelRow(ChannelRow):
def compose(self) -> ComposeResult: def compose(self) -> ComposeResult:
with HorizontalScroll(): with HorizontalScroll():
yield Channel('AN1', phantom=True) yield Channel(self.channels[0], 'AN1', phantom=True)
yield Channel('AN2', phantom=True, inst=True) yield Channel(self.channels[1], 'AN2', phantom=True, inst=True)
yield Channel('ADAT1') for n in range(0, 8):
yield Channel('ADAT2') yield Channel(channel=self.channels[n+2], label='ADAT{n}'.format(n=n+1))
yield Channel('ADAT3')
yield Channel('ADAT4') def update_mixer_display(self, mixer: MixerMatrix, current_out: int):
yield Channel('ADAT5') for channel_widget, levels, mute, solo in zip(
yield Channel('ADAT6') self.query(Channel),
yield Channel('ADAT7') mixer.matrix[:self.channels[-1]],
yield Channel('ADAT8') mixer.input_mutes[:self.channels[-1]],
mixer.input_solos[:self.channels[-1]]):
channel_widget.level = levels[current_out]
channel_widget.mute = mute
channel_widget.solo = solo
class PlaybackChannelRow(ChannelRow): class PlaybackChannelRow(ChannelRow):
LABELS = [
'AN1',
'AN2',
'PH3',
'PH4',
'ADAT1',
'ADAT2',
'ADAT3',
'ADAT4',
'ADAT5',
'ADAT6',
'ADAT7',
'ADAT8',
]
def compose(self) -> ComposeResult: def compose(self) -> ComposeResult:
with HorizontalScroll(): with HorizontalScroll():
yield Channel('AN1') for n, label in zip(self.channels, self.LABELS):
yield Channel('AN2') yield Channel(channel=n, label=label)
yield Channel('PH3')
yield Channel('PH4') def update_mixer_display(self, mixer: MixerMatrix, current_out: int):
yield Channel('ADAT1') for channel_widget, levels, mute, solo in zip(
yield Channel('ADAT2') self.query(Channel),
yield Channel('ADAT3') mixer.matrix[self.channels[0]:self.channels[-1]],
yield Channel('ADAT4') mixer.input_mutes[self.channels[0]:self.channels[-1]],
yield Channel('ADAT5') mixer.input_solos[self.channels[0]:self.channels[-1]]):
yield Channel('ADAT6') channel_widget.level = levels[current_out]
yield Channel('ADAT7') channel_widget.mute = mute
yield Channel('ADAT8') channel_widget.solo = solo
class OutputChannelRow(ChannelRow): class OutputChannelRow(ChannelRow):
LABELS = [
'AN1',
'AN2',
'PH3',
'PH4',
'ADAT1',
'ADAT2',
'ADAT3',
'ADAT4',
'ADAT5',
'ADAT6',
'ADAT7',
'ADAT8',
]
def compose(self) -> ComposeResult: def compose(self) -> ComposeResult:
with HorizontalScroll(): with HorizontalScroll():
yield Channel('AN1') with HorizontalScroll():
yield Channel('AN2') for n, label in zip(self.channels, self.LABELS):
yield Channel('PH3') yield Channel(channel=n, label=label, output=True)
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')
def update_mixer_display(self, mixer: MixerMatrix, current_out: int):
for idx, channel_widget, level, mute, solo in zip(
self.channels,
self.query(Channel),
mixer.outputs,
mixer.output_mutes,
mixer.output_solos):
channel_widget.level = level
channel_widget.mute = mute
channel_widget.solo = solo
channel_widget.selected = idx == current_out
class BFStatus: class BFStatus:
word0 = 0 word0 = 0
@ -315,20 +575,28 @@ class BFStatus:
if len(msg) > 2: if len(msg) > 2:
self.word1 = format(msg[1], '032b') self.word1 = format(msg[1], '032b')
self.word2 = format(msg[2], '032b') self.word2 = format(msg[2], '032b')
self.ch1_48v = get_bitfield(msg[0], 31, 1) self.ch1_48v = get_bitfield(msg[0], 30, 1)
self.ch2_48v = get_bitfield(msg[0], 30, 1) self.ch2_48v = get_bitfield(msg[0], 31, 1)
self.ch2_inst = get_bitfield(msg[0], 29, 1) self.ch2_inst = get_bitfield(msg[0], 29, 1)
self.digital_out = get_bitfield(msg[0], 28, 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_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.out_2_vol = level_to_db(get_bitfield(msg[1], 9, 8))
self.ch2_gain = get_bitfield(msg[1], 24, 5) self.ch2_gain = get_bitfield(msg[1], 24, 5) * 3 + 6
self.ch1_gain = get_bitfield(msg[1], 19, 5) self.ch1_gain = get_bitfield(msg[1], 19, 5) * 3 + 6
self.out_3_vol = level_to_db(get_bitfield(msg[2], 15, 8)) self.out_3_vol = level_to_db(get_bitfield(msg[2], 14, 8))
self.out_4_vol = level_to_db(get_bitfield(msg[2], 23, 8)) self.out_4_vol = level_to_db(get_bitfield(msg[2], 23, 8))
self.dim = get_bitfield(msg[2], 5, 1) self.dim = get_bitfield(msg[2], 5, 1)
def out_levels(self):
return (
self.out_1_vol,
self.out_2_vol,
self.out_3_vol,
self.out_4_vol,
)
class BFRMS: class BFRMS:
def __init__(self): def __init__(self):
@ -354,6 +622,7 @@ class BFRMS:
self._parse_rms(id1_msg[10:34], self.output_rms) self._parse_rms(id1_msg[10:34], self.output_rms)
self._parse_rms(id1_msg[34:38], self.fx_in_rms) self._parse_rms(id1_msg[34:38], self.fx_in_rms)
class BFPeak: class BFPeak:
def __init__(self): def __init__(self):
self.input_peaks = [0 for _ in range(10)] self.input_peaks = [0 for _ in range(10)]
@ -382,13 +651,27 @@ class Status(Widget):
class Mixer(App): class Mixer(App):
in_port: mido.ports.BaseInput in_port: mido.ports.BaseInput
out_port: mido.ports.BaseInput out_port: mido.ports.BaseOutput
messages = [] messages = []
messageset = set() messageset = set()
STATUS_MSG = mido.Message('sysex', data=[0x00, 0x20, 0x0D, 0x10, 0x10]) STATUS_MSG = mido.Message('sysex', data=[0x00, 0x20, 0x0D, 0x10, 0x10])
bf_status = BFStatus() bf_status = BFStatus()
bf_rms = BFRMS() bf_rms = BFRMS()
bf_peak = BFPeak() bf_peak = BFPeak()
current_mix_out = 0
focused_channel = (0, 2)
INPUTS = 10
PLAYBACKS = 12
OUTPUTS = 12
BINDINGS = [
('o', 'mix_select(1)'),
('u', 'mix_select(-1)'),
('h', 'change_focus(-1, 0)'),
('j', 'change_focus(0, 1)'),
('k', 'change_focus(0, -1)'),
('l', 'change_focus(1, 0)'),
]
def __init__(self): def __init__(self):
super().__init__() super().__init__()
@ -404,6 +687,8 @@ class Mixer(App):
else: else:
self.out_port = mido.open_output('Mixer', virtual=True) self.out_port = mido.open_output('Mixer', virtual=True)
self.mixer = MixerMatrix(self.out_port)
def on_mount(self) -> None: def on_mount(self) -> None:
self.set_interval(.1, self.update_messages) self.set_interval(.1, self.update_messages)
@ -414,11 +699,83 @@ class Mixer(App):
def compose(self) -> ComposeResult: def compose(self) -> ComposeResult:
yield Header() yield Header()
yield InputChannelRow() yield InputChannelRow(channels=range(0, self.INPUTS))
yield PlaybackChannelRow() yield PlaybackChannelRow(channels=range(self.INPUTS, self.PLAYBACKS + self.INPUTS))
yield OutputChannelRow() yield OutputChannelRow(channels=range(0, self.OUTPUTS))
yield Footer() yield Footer()
"""
Change the mix output
"""
def action_mix_select(self, n):
self.current_mix_out = (self.current_mix_out + n) % self.OUTPUTS
self.update_mixer_display()
"""
Update focused channel. Mix output follows focus.
"""
def action_change_focus(self, dx, dy):
(x, y) = self.focused_channel
y = (y + dy) % 3
x = x + dx
if y == 0:
x = x % self.INPUTS
elif y == 1:
x = x % self.PLAYBACKS
elif y == 2:
x = x % self.OUTPUTS
self.current_mix_out = x
self.focused_channel = (x, y)
ch = self.query(ChannelRow)[y].query(Channel)[x]
self.query(ChannelRow)[y].query(Channel)[x].focus()
self.update_mixer_display()
def _move_fader(self, dval):
self.update_mixer_display()
def on_channel_phantom_toggle(self, message: Channel.PhantomToggle) -> None:
if message.channel == 0:
self.out_port.send(encode_message(3, [0x00010001 ^ self.bf_status.ch1_48v << 16]))
elif message.channel == 1:
self.out_port.send(encode_message(3, [0x00020002 ^ self.bf_status.ch2_48v << 17]))
def on_channel_inst_toggle(self, message: Channel.PhantomToggle) -> None:
if message.channel == 1:
self.out_port.send(encode_message(3, [0x00100010 ^ self.bf_status.ch2_inst << 20]))
def on_channel_move_fader(self, message: Channel.MoveFader) -> None:
if message.output:
self.mixer.set_out_level(message.channel, self.mixer.outputs[message.channel] + message.change)
else:
self.mixer.set_mix_level(message.channel, self.current_mix_out,
self.mixer.matrix[message.channel][self.current_mix_out] + message.change)
self.update_mixer_display()
def on_channel_change_gain(self, message: Channel.ChangeGain) -> None:
if message.channel > 1:
return
if message.channel == 0:
gain = clamp(message.change + self.bf_status.ch1_gain, 6, 60)
elif message.channel == 1:
gain = clamp(message.change + self.bf_status.ch2_gain, 6, 60)
gain_scaled = int((gain - 6) / 3)
self.out_port.send(encode_message(4, [gain_scaled << 16 | message.channel]))
def on_channel_mute_toggle(self, message: Channel.MuteToggle) -> None:
if message.output:
self.mixer.toggle_out_mute_status(message.channel)
else:
self.mixer.toggle_mix_mute_status(message.channel)
self.update_mixer_display()
def update_mixer_display(self):
for row in self.query(ChannelRow):
row.update_mixer_display(self.mixer, self.current_mix_out)
def process_message(self, msg: mido.Message) -> None: def process_message(self, msg: mido.Message) -> None:
subid, decoded_msg = decode_message(msg) subid, decoded_msg = decode_message(msg)
if subid is None: if subid is None:
@ -434,25 +791,36 @@ class Mixer(App):
self.messages.append(hex_format(decoded_msg)) self.messages.append(hex_format(decoded_msg))
self.query_one('#log').update(self.messages) self.query_one('#log').update(self.messages)
return
input_row = self.query_one(InputChannelRow) input_row = self.query_one(InputChannelRow)
input_row.rms_values = self.bf_rms.input_rms input_row.rms_values = self.bf_rms.input_rms
input_row.peak_values = self.bf_peak.input_peaks input_row.peak_values = self.bf_peak.input_peaks
input_row.mutate_reactive(ChannelRow.rms_values) channels = input_row.query(Channel)
input_row.mutate_reactive(ChannelRow.peak_values) channels[0].phantom = self.bf_status.ch1_48v
channels[1].phantom = self.bf_status.ch2_48v
channels[1].inst = self.bf_status.ch2_inst
channels[0].gain = self.bf_status.ch1_gain
channels[1].gain = self.bf_status.ch2_gain
playback_row = self.query_one(PlaybackChannelRow) playback_row = self.query_one(PlaybackChannelRow)
playback_row.rms_values = self.bf_rms.playback_rms playback_row.rms_values = self.bf_rms.playback_rms
playback_row.peak_values = self.bf_peak.playback_peaks 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 = self.query_one(OutputChannelRow)
output_row.rms_values = self.bf_rms.output_rms output_row.rms_values = self.bf_rms.output_rms
output_row.peak_values = self.bf_peak.output_peaks output_row.peak_values = self.bf_peak.output_peaks
output_row.mutate_reactive(ChannelRow.rms_values)
output_row.mutate_reactive(ChannelRow.peak_values) dirty_faders = False
#self.query_one('#status').update(self.bf_status.__dict__) for n, mixvol in enumerate(self.bf_status.out_levels()):
if self.mixer.outputs[n] != mixvol:
self.mixer.outputs[n] = mixvol
dirty_faders = True
if dirty_faders:
self.update_mixer_display()
for row in [input_row, playback_row, output_row]:
row.mutate_reactive(ChannelRow.rms_values)
row.mutate_reactive(ChannelRow.peak_values)
if __name__ == '__main__': if __name__ == '__main__':