Added basic functionality
- gain - mute - mixer - channel setup options - keybinds and mousebinds - focus behavior
This commit is contained in:
parent
f444daeeca
commit
04060ef5a1
2 changed files with 491 additions and 108 deletions
19
encoding.py
19
encoding.py
|
|
@ -63,12 +63,21 @@ def set_bitfield(word, val, position, length):
|
|||
return (word & mask << position) | val
|
||||
|
||||
|
||||
"""
|
||||
Special level calculation for main/ph outputs
|
||||
"""
|
||||
def level_to_db(val):
|
||||
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):
|
||||
|
|
@ -76,3 +85,9 @@ def format_db(val, min_db=-120):
|
|||
return '∞'
|
||||
else:
|
||||
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)
|
||||
574
main.py
574
main.py
|
|
@ -1,51 +1,113 @@
|
|||
import math
|
||||
from collections import namedtuple
|
||||
|
||||
from rich.console import ConsoleOptions, Console
|
||||
from rich.segment import Segment
|
||||
from rich.style import Style
|
||||
from textual import events
|
||||
from textual.app import App, ComposeResult, Widget, RenderResult
|
||||
from textual.color import Color
|
||||
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.strip import Strip
|
||||
from textual.widgets import Static, Button, Pretty, Log, Footer, Header
|
||||
from textual.messages import Message
|
||||
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
|
||||
|
||||
ChannelState = namedtuple('ChannelState', ['level', 'muted', 'soloed'])
|
||||
|
||||
|
||||
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)]
|
||||
self.matrix = [[ -120.0 for _ in range(out_channels) ] for _ in range(in_channels)] + [
|
||||
[ 0.0 if p == o else -120.0 for p in range(play_channels)
|
||||
] for o in range(out_channels)]
|
||||
|
||||
def update_out(self, channel, val):
|
||||
self.out_levels[channel] = val
|
||||
self.outputs = [0.0 for _ in range(out_channels)]
|
||||
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):
|
||||
self.in_levels[out_channel][in_channel] = val
|
||||
self._send_update(in_channel, out_channel, val)
|
||||
def toggle_out_solo_status(self, channel):
|
||||
solo = self.output_solos[channel] = not self.output_solos[channel]
|
||||
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):
|
||||
self.play_levels[out_channel][play_channel] = val
|
||||
self._send_update(play_channel + self.in_channels, out_channel, val)
|
||||
def toggle_out_mute_status(self, channel):
|
||||
mute = self.output_mutes[channel] = not self.output_mutes[channel]
|
||||
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
|
||||
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))
|
||||
msg = encoding.db_to_fader(db_val) << 12 | reg
|
||||
|
||||
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
|
||||
msg = val << 15 | reg
|
||||
self.out_port.send(encode_message(encoding.SUBID_MIXER, msg))
|
||||
msg = encoding.db_to_fader(db_val) << 12 | reg
|
||||
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:
|
||||
FRACTIONS = [
|
||||
|
|
@ -65,7 +127,6 @@ class MeterGradient:
|
|||
self._color2 = Color.parse(color2)
|
||||
self._value = 1 - value
|
||||
|
||||
|
||||
def __rich_console__(
|
||||
self, console: Console, options: ConsoleOptions
|
||||
) -> RenderResult:
|
||||
|
|
@ -102,6 +163,7 @@ class Meter(Widget):
|
|||
}
|
||||
"""
|
||||
value = reactive(1.0)
|
||||
|
||||
def render(self) -> RenderResult:
|
||||
return MeterGradient(self.value, '#04ff00', '#04aa00')
|
||||
|
||||
|
|
@ -109,7 +171,7 @@ class Meter(Widget):
|
|||
class Channel(Widget):
|
||||
DEFAULT_CSS = """
|
||||
Channel {
|
||||
width: 11;
|
||||
width: 12;
|
||||
border: solid #333333;
|
||||
Horizontal {
|
||||
height: 100%;
|
||||
|
|
@ -129,17 +191,17 @@ class Channel(Widget):
|
|||
min-width: 6;
|
||||
height: 1;
|
||||
}
|
||||
.mute {
|
||||
.mute.enabled {
|
||||
background: #00f !important;
|
||||
}
|
||||
.solo {
|
||||
background: #0ff;
|
||||
.solo.enabled {
|
||||
background: #0ff !important;
|
||||
}
|
||||
.phantom {
|
||||
background: #f00;
|
||||
.phantom.enabled {
|
||||
background: #f00 !important;
|
||||
}
|
||||
.inst {
|
||||
background: #ff0;
|
||||
.inst.enabled {
|
||||
background: #ff0 !important;
|
||||
}
|
||||
.indicators-wrap {
|
||||
margin-left: 1;
|
||||
|
|
@ -155,19 +217,85 @@ class Channel(Widget):
|
|||
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)
|
||||
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)
|
||||
class MoveFader(Message):
|
||||
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:
|
||||
try:
|
||||
|
|
@ -181,41 +309,128 @@ class Channel(Widget):
|
|||
except ValueError:
|
||||
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-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:
|
||||
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
|
||||
self.query_one(Meter).value = encoding.map_range(val, -75, 6, 0, 1)
|
||||
|
||||
def compose(self) -> ComposeResult:
|
||||
yield Static(self.label, classes='label')
|
||||
with Horizontal():
|
||||
yield Meter()
|
||||
yield Meter(id='meter')
|
||||
yield FaderIndicator(id='level-indicator')
|
||||
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)
|
||||
yield Button("M", classes='mute', compact=True, id='mute')
|
||||
yield Button("S", classes='solo', compact=True, id='solo')
|
||||
if self.phantom_available:
|
||||
yield Button("48v", classes='phantom', compact=True, id='phantom')
|
||||
if self.inst_available:
|
||||
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 Container(classes='indicators'):
|
||||
yield Static('0.0', classes='level')
|
||||
yield Static('0.0', classes='peak')
|
||||
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):
|
||||
DEFAULT_CSS = """
|
||||
|
|
@ -228,6 +443,10 @@ class ChannelRow(Widget):
|
|||
peak_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):
|
||||
for channel, value in zip(self.query(Channel), new_values):
|
||||
channel.rms_value = value
|
||||
|
|
@ -240,52 +459,93 @@ class ChannelRow(Widget):
|
|||
for channel, value in zip(self.query(Channel), new_values):
|
||||
channel.level_value = value
|
||||
|
||||
def update_mixer_display(self, mixer: MixerMatrix, current_out: int):
|
||||
pass
|
||||
|
||||
|
||||
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')
|
||||
yield Channel(self.channels[0], 'AN1', phantom=True)
|
||||
yield Channel(self.channels[1], 'AN2', phantom=True, inst=True)
|
||||
for n in range(0, 8):
|
||||
yield Channel(channel=self.channels[n+2], label='ADAT{n}'.format(n=n+1))
|
||||
|
||||
def update_mixer_display(self, mixer: MixerMatrix, current_out: int):
|
||||
for channel_widget, levels, mute, solo in zip(
|
||||
self.query(Channel),
|
||||
mixer.matrix[:self.channels[-1]],
|
||||
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):
|
||||
LABELS = [
|
||||
'AN1',
|
||||
'AN2',
|
||||
'PH3',
|
||||
'PH4',
|
||||
'ADAT1',
|
||||
'ADAT2',
|
||||
'ADAT3',
|
||||
'ADAT4',
|
||||
'ADAT5',
|
||||
'ADAT6',
|
||||
'ADAT7',
|
||||
'ADAT8',
|
||||
]
|
||||
|
||||
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')
|
||||
for n, label in zip(self.channels, self.LABELS):
|
||||
yield Channel(channel=n, label=label)
|
||||
|
||||
def update_mixer_display(self, mixer: MixerMatrix, current_out: int):
|
||||
for channel_widget, levels, mute, solo in zip(
|
||||
self.query(Channel),
|
||||
mixer.matrix[self.channels[0]:self.channels[-1]],
|
||||
mixer.input_mutes[self.channels[0]:self.channels[-1]],
|
||||
mixer.input_solos[self.channels[0]:self.channels[-1]]):
|
||||
channel_widget.level = levels[current_out]
|
||||
channel_widget.mute = mute
|
||||
channel_widget.solo = solo
|
||||
|
||||
class OutputChannelRow(ChannelRow):
|
||||
LABELS = [
|
||||
'AN1',
|
||||
'AN2',
|
||||
'PH3',
|
||||
'PH4',
|
||||
'ADAT1',
|
||||
'ADAT2',
|
||||
'ADAT3',
|
||||
'ADAT4',
|
||||
'ADAT5',
|
||||
'ADAT6',
|
||||
'ADAT7',
|
||||
'ADAT8',
|
||||
]
|
||||
|
||||
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')
|
||||
with HorizontalScroll():
|
||||
for n, label in zip(self.channels, self.LABELS):
|
||||
yield Channel(channel=n, label=label, output=True)
|
||||
|
||||
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:
|
||||
word0 = 0
|
||||
|
|
@ -315,20 +575,28 @@ class BFStatus:
|
|||
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.ch1_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.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.ch2_gain = get_bitfield(msg[1], 24, 5) * 3 + 6
|
||||
self.ch1_gain = get_bitfield(msg[1], 19, 5) * 3 + 6
|
||||
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.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:
|
||||
def __init__(self):
|
||||
|
|
@ -354,6 +622,7 @@ class BFRMS:
|
|||
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)]
|
||||
|
|
@ -382,13 +651,27 @@ class Status(Widget):
|
|||
|
||||
class Mixer(App):
|
||||
in_port: mido.ports.BaseInput
|
||||
out_port: mido.ports.BaseInput
|
||||
out_port: mido.ports.BaseOutput
|
||||
messages = []
|
||||
messageset = set()
|
||||
STATUS_MSG = mido.Message('sysex', data=[0x00, 0x20, 0x0D, 0x10, 0x10])
|
||||
bf_status = BFStatus()
|
||||
bf_rms = BFRMS()
|
||||
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):
|
||||
super().__init__()
|
||||
|
|
@ -404,6 +687,8 @@ class Mixer(App):
|
|||
else:
|
||||
self.out_port = mido.open_output('Mixer', virtual=True)
|
||||
|
||||
self.mixer = MixerMatrix(self.out_port)
|
||||
|
||||
def on_mount(self) -> None:
|
||||
self.set_interval(.1, self.update_messages)
|
||||
|
||||
|
|
@ -414,11 +699,83 @@ class Mixer(App):
|
|||
|
||||
def compose(self) -> ComposeResult:
|
||||
yield Header()
|
||||
yield InputChannelRow()
|
||||
yield PlaybackChannelRow()
|
||||
yield OutputChannelRow()
|
||||
yield InputChannelRow(channels=range(0, self.INPUTS))
|
||||
yield PlaybackChannelRow(channels=range(self.INPUTS, self.PLAYBACKS + self.INPUTS))
|
||||
yield OutputChannelRow(channels=range(0, self.OUTPUTS))
|
||||
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:
|
||||
subid, decoded_msg = decode_message(msg)
|
||||
if subid is None:
|
||||
|
|
@ -434,25 +791,36 @@ class Mixer(App):
|
|||
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)
|
||||
channels = input_row.query(Channel)
|
||||
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.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__)
|
||||
|
||||
dirty_faders = False
|
||||
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__':
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue