Select Git revision
mixer.py 12.37 KiB
import bl00mbox
from st3m import Responder, settings
import math
import media
def recall_blm_channel_stats(blm_chan):
if blm_chan.free:
return
if blm_chan.name in session_channel_vol_mute:
chan = bl00mboxChannel(bl00mbox.SysChannel(blm_chan))
vol, mute = session_channel_vol_mute[chan.get_name()]
if mute:
vol = -math.inf
chan._set_vol_dB(vol)
bl00mbox.set_channel_init_callback(recall_blm_channel_stats)
class Channel:
index = 0
_vol = 0
def get_name(self):
return "dummy" + str(self.index)
def get_num_plugins(self):
return None
def _get_vol_dB(self):
return self._vol
def _set_vol_dB(self, val):
self._vol = val
def __init__(self):
self.mute = False
def get_rms_dB(self):
return -math.inf
def get_mute(self):
mute = False
if self.get_name() in session_channel_vol_mute:
_, mute = session_channel_vol_mute[self.get_name()]
return bool(mute)
def set_mute(self, mute):
if self.get_name() in session_channel_vol_mute:
vol, _ = session_channel_vol_mute[self.get_name()]
if mute:
vol = self._get_vol_dB()
self._set_vol_dB(-math.inf)
else:
self._set_vol_dB(vol)
session_channel_vol_mute[self.get_name()] = vol, mute
elif mute:
vol = self._get_vol_dB()
session_channel_vol_mute[self.get_name()] = vol, True
self._set_vol_dB(-math.inf)
def get_vol_dB(self):
if self.get_mute():
if self.get_name() in session_channel_vol_mute:
vol, _ = session_channel_vol_mute[self.get_name()]
return vol
else:
return -math.inf
else:
return self._get_vol_dB()
def set_vol_dB(self, vol):
if not self.get_mute():
self._set_vol_dB(vol)
session_channel_vol_mute[self.get_name()] = vol, self.get_mute()
class bl00mboxChannel(Channel):
def __init__(self, chan):
super().__init__()
self.blm = chan
self._name = chan.name
self._num_plugins = chan.num_plugins
def get_name(self):
return self._name
def get_num_plugins(self):
return self._num_plugins
def _get_vol_dB(self):
return self.blm.sys_gain_dB
def _set_vol_dB(self, vol):
self.blm.sys_gain_dB = vol
def get_rms_dB(self):
return self.blm.sys_rms_dB
class mediaChannel(Channel):
def get_name(self):
return "media"
def _get_vol_dB(self):
ret = media.get_volume()
if ret == 0:
return -math.inf
else:
return 20 * math.log(ret, 10)
def _set_vol_dB(self, vol):
if vol == -math.inf:
vol = 0
else:
vol = 10 ** (vol / 20)
media.set_volume(vol)
session_channel_vol_mute = {}
class ChannelColors:
bg = (0.3, 0.3, 0.3)
name = (0, 1, 1)
vol_bar = (0, 1, 0)
rms_bar = (1, 0, 0)
mute_bg = ((0.2, 0.2, 0.2), (0.5, 0.0, 0.0))
mute_fg = ((0.5, 0.5, 0.5), (0.8, 0.8, 0.8))
class HighlightColors:
bg = (0.5, 0.5, 0.5)
name = (0, 1, 1)
vol_bar = (0, 1, 0)
rms_bar = (1, 0, 0)
mute_bg = ((0.2, 0.2, 0.2), (0.8, 0.0, 0.0))
mute_fg = ((0.5, 0.5, 0.5), (1.0, 1.0, 1.0))
class AudioMixer(Responder):
def __init__(self, inputcontroller):
self.full_redraw = True
self.input = inputcontroller
self._pos = 0
self.chan_width = 46 # scaling factor between _pos and draw_pos
self._editing_channel = 0
self.colors = ChannelColors()
self.highlight_colors = HighlightColors()
self._refresh()
self._draw_pos = [
self.chan_width * max(min((len(self._chans) - 1) / 2, 1), 0),
0,
0,
] # includes also 1st and 2nd derivatives per second(^2)
self.override_os_button_back = False
repeat = settings.num_volume_repeat_ms.value
repeat_wait = settings.num_volume_repeat_wait_ms.value
self.vol_repeat = (repeat_wait, repeat)
self.channel_select_repeat = (500, 300)
self.volume_step_dB = settings.num_volume_step_db.value
self.volume_repeat_step_dB = settings.num_volume_repeat_step_db.value
self.input.buttons.app.right.repeat_enable(*self.channel_select_repeat)
self.input.buttons.app.left.repeat_enable(*self.channel_select_repeat)
self.acc_ms = 0
# we don't have double-volume here yet so we share it with applications.
# if it has been changed by an application we accept that as the new value
# for now.
if "media" in session_channel_vol_mute:
vol, mute = session_channel_vol_mute["media"]
mediavol = media.get_volume()
if mute:
if mediavol != 0:
mute = False
vol = mediavol
else:
vol = mediavol
session_channel_vol_mute["media"] = (vol, mute)
def _refresh(self):
self._chans = [mediaChannel()]
for c in bl00mbox.Sys.collect_channels(True):
chan = bl00mboxChannel(c)
chan.prev_compute_rms = chan.blm.compute_rms
chan.blm.compute_rms = True
self._chans += [chan]
"""
for i in range(5):
self._chans += [Channel()]
"""
for i, chan in enumerate(self._chans):
chan.index = i
self._pos = min(self._pos, len(self._chans) - 1)
def think(self, ins, delta_ms):
self.override_os_button_back = bool(self._editing_channel)
lr_dir = self.input.buttons.app.right.pressed
lr_dir -= self.input.buttons.app.left.pressed
num_chans = len(self._chans)
if self.input.buttons.app.middle.pressed:
self.full_redraw = True
self._editing_channel += 1
self._editing_channel %= 3
if self._editing_channel == 1:
self.input.buttons.app.right.repeat_enable(*self.vol_repeat)
self.input.buttons.app.left.repeat_enable(*self.vol_repeat)
elif not self._editing_channel:
self.input.buttons.app.right.repeat_enable(*self.channel_select_repeat)
self.input.buttons.app.left.repeat_enable(*self.channel_select_repeat)
elif self._editing_channel == 1:
lr_dir_repeat = self.input.buttons.app.right.repeated
lr_dir_repeat -= self.input.buttons.app.left.repeated
lr_dir_repeat *= self.volume_repeat_step_dB
lr_dir *= self.volume_step_dB
lr_dir += lr_dir_repeat
if self._editing_channel:
if self.input.buttons.os.middle.released:
self._editing_channel = 0
elif lr_dir and num_chans > self._pos:
chan = self._chans[self._pos]
if self._editing_channel == 1:
vol = chan.get_vol_dB() + lr_dir
vol = min(20, max(-50, vol))
chan.set_vol_dB(vol)
if self._editing_channel == 2:
chan.set_mute(not chan.get_mute())
else:
lr_dir += self.input.buttons.app.right.repeated
lr_dir -= self.input.buttons.app.left.repeated
self._pos += lr_dir
self._pos %= num_chans
if lr_dir:
self.full_redraw = True
self.acc_ms += delta_ms
if num_chans > 2:
target = min(num_chans - 2, max(self._pos, 1))
elif num_chans == 2:
target = 0.5
else:
target = 0
target *= self.chan_width
self.acc_ms += delta_ms
pos_prev = self._draw_pos[0]
while self.acc_ms > 0:
self.acc_ms -= 20
self._draw_pos[2] = (target - self._draw_pos[0]) / 2
self._draw_pos[2] -= self._draw_pos[1]
self._draw_pos[1] += self._draw_pos[2] / 10
self._draw_pos[0] += self._draw_pos[1] / 10
self._draw_pos[0] = round(self._draw_pos[0], 1)
if not self.full_redraw:
self.full_redraw = self._draw_pos[0] != pos_prev
def draw(self, ctx):
ctx.text_align = ctx.CENTER
ctx.rgb(0x81 / 255, 0xCD / 255, 0xC6 / 255)
if self.full_redraw:
ctx.rgb(0, 0, 0)
ctx.rectangle(-120, -120, 240, 240).fill()
ctx.move_to(0, -90)
ctx.font_size = 24
ctx.text("~ mixer ~")
for x, chan in enumerate(self._chans):
xpos = x * self.chan_width - self._draw_pos[0]
if abs(xpos) > 120 + self.chan_width / 2:
continue
highlight = self._pos == x
if highlight:
colors = self.highlight_colors
else:
colors = self.colors
ctx.save()
ctx.translate(xpos, (1 - highlight) * 2)
self.draw_channel(colors, ctx, chan, highlight)
ctx.restore()
if self.full_redraw:
self.full_redraw = False
def draw_channel(self, colors, ctx, chan, highlight):
ctx.text_align = ctx.RIGHT
ctx.rgb(*colors.bg)
chan_len = 160
chan_upper = -80
chan_width = self.chan_width - 4
len_bar = chan_len - 38
bar_bottom = chan_upper + chan_len - 21
if self.full_redraw:
ctx.round_rectangle(
-chan_width / 2, chan_upper, chan_width, chan_len, 2
).fill()
# draw name
ctx.font_size = 16
ctx.save()
ctx.rgb(*colors.name)
ctx.translate(0, 0)
ctx.rotate(-math.tau / 4)
ctx.move_to(-chan_upper - 4, -chan_width / 2 + 14)
name = chan.get_name()
while ctx.text_width(name) > 110:
name = name[:-1]
ctx.text(name)
ctx.restore()
# draw number of plugins (bl00mbox only)
num_plugins = chan.get_num_plugins()
if num_plugins is not None:
ctx.font_size = 12
ctx.rgb(*[0.7 * x for x in colors.bg])
ctx.move_to(chan_width / 2 - 3, chan_upper + 12)
ctx.text(str(num_plugins))
# volume
vol = chan.get_vol_dB()
ctx.rgb(0, 0, 0)
ctx.move_to(chan_width / 2 - 3, 75)
ctx.font_size = 14
ctx.text(f"{vol:.1f}")
# mute
mute = int(chan.get_mute())
ctx.rgb(*(colors.mute_bg[mute]))
ctx.rectangle(-chan_width / 2 + 3, 41, 18, 18).fill()
ctx.text_align = ctx.CENTER
ctx.font_size = 16
ctx.move_to(-chan_width / 2 + 12, 55)
ctx.rgb(*(colors.mute_fg[mute]))
ctx.text("M")
if self._editing_channel == 2 and highlight:
ctx.rgb(0, 1, 0)
ctx.round_rectangle(-chan_width / 2 + 3, 41, 18, 18, 2).stroke()
ctx.rgb(0, 0, 0)
ctx.rectangle(chan_width / 2 - 13, bar_bottom - len_bar, 9, len_bar).fill()
ctx.move_to(chan_width / 2 - 13, bar_bottom - len_bar * 5 / 7)
ctx.rel_line_to(-5, 0).stroke()
# vol bar
ctx.rgb(*colors.vol_bar)
vol_bar_len = (vol + 50) / 70
vol_bar_len = min(1, max(0, vol_bar_len))
vol_bar_len *= len_bar
ctx.rectangle(
chan_width / 2 - 12, bar_bottom - vol_bar_len, 3, vol_bar_len
).fill()
if self._editing_channel == 1 and highlight:
ctx.rgb(0, 1, 0)
ctx.move_to(6, bar_bottom - vol_bar_len)
ctx.rel_line_to(-5, -3)
ctx.rel_line_to(0, 6)
ctx.rel_line_to(5, -3)
ctx.fill()
ctx.rgb(0, 0, 0)
ctx.rectangle(chan_width / 2 - 9, bar_bottom - len_bar, 5, len_bar).fill()
# rms bar
# hmmmh do we reuse the 0dB notch for normalization...?
rms = chan.get_rms_dB() + 12
ctx.rgb(*colors.rms_bar)
rms_bar_len = (rms + 50) / 70
rms_bar_len = min(1, max(0, rms_bar_len))
rms_bar_len *= len_bar
ctx.rectangle(
chan_width / 2 - 8, bar_bottom - rms_bar_len, 3, rms_bar_len
).fill()
def on_exit(self):
for chan in self._chans:
if isinstance(chan, bl00mboxChannel):
chan.blm.compute_rms = chan.prev_compute_rms