Update Language-Team tag in German .po file
[jack_mixer.git] / jack_mixer / channel.py
blob31907179159bf661ac6b8e489312e5de201cb4f8
1 # This file is part of jack_mixer
3 # Copyright (C) 2006 Nedko Arnaudov <nedko@arnaudov.name>
5 # This program is free software; you can redistribute it and/or modify
6 # it under the terms of the GNU General Public License as published by
7 # the Free Software Foundation; version 2 of the License
9 # This program is distributed in the hope that it will be useful,
10 # but WITHOUT ANY WARRANTY; without even the implied warranty of
11 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
12 # GNU General Public License for more details.
14 # You should have received a copy of the GNU General Public License
15 # along with this program; if not, write to the Free Software
16 # Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA.
18 import logging
20 import gi # noqa: F401
21 from gi.repository import Gtk
22 from gi.repository import Gdk
23 from gi.repository import GObject
24 from gi.repository import Pango
26 from . import abspeak
27 from . import meter
28 from . import slider
29 from .serialization import SerializedObject
30 from .styling import set_background_color, random_color
33 log = logging.getLogger(__name__)
34 BUTTON_PADDING = 1
37 class Channel(Gtk.Box, SerializedObject):
38 """Widget with slider and meter used as base class for more specific
39 channel widgets"""
41 num_instances = 0
43 def __init__(self, app, name, stereo=True, direct_output=True, initial_vol=None):
44 super().__init__(orientation=Gtk.Orientation.VERTICAL)
45 self.app = app
46 self.mixer = app.mixer
47 self.channel = None
48 self.gui_factory = app.gui_factory
49 self._channel_name = name
50 self.stereo = stereo
51 self.initial_vol = initial_vol
52 self.meter_scale = self.gui_factory.get_default_meter_scale()
53 self.slider_scale = self.gui_factory.get_default_slider_scale()
54 self.slider_adjustment = slider.AdjustmentdBFS(self.slider_scale, 0.0, 0.02)
55 self.balance_adjustment = slider.BalanceAdjustment()
56 self.wants_direct_output = direct_output
57 self.post_fader_output_channel = None
58 self.future_out_mute = None
59 self.future_volume_midi_cc = None
60 self.future_balance_midi_cc = None
61 self.future_mute_midi_cc = None
62 self.future_solo_midi_cc = None
63 self.css_name = "css_name_%d" % Channel.num_instances
64 self.label_name = None
65 self.wide = True
66 self.label_chars_wide = 12
67 self.label_chars_narrow = 7
68 self.channel_properties_dialog = None
69 self.monitor_button = None
70 Channel.num_instances += 1
72 # ---------------------------------------------------------------------------------------------
73 # Properties
75 @property
76 def channel_name(self):
77 return self._channel_name
79 @channel_name.setter
80 def channel_name(self, name):
81 self.app.on_channel_rename(self._channel_name, name)
82 self._channel_name = name
83 if self.label_name:
84 self.label_name.set_text(name)
85 if len(name) > (self.label_chars_wide if self.wide else self.label_chars_narrow):
86 self.label_name.set_tooltip_text(name)
87 if self.channel:
88 self.channel.name = name
89 if self.post_fader_output_channel:
90 self.post_fader_output_channel.name = _("{channel_name} Out").format(channel_name=name)
92 # ---------------------------------------------------------------------------------------------
93 # UI creation and (de-)initialization
95 def create_balance_widget(self):
96 parent = None
97 if self.balance:
98 parent = self.balance.get_parent()
99 self.balance.destroy()
101 self.balance = slider.BalanceSlider(self.balance_adjustment, (20, 20), (0, 100))
103 if parent:
104 parent.pack_end(self.balance, False, True, 0)
106 self.balance.show()
108 def create_buttons(self):
109 # Mute, Solo and Monitor buttons
110 self.hbox_mutesolo = Gtk.Box(False, 0, orientation=Gtk.Orientation.HORIZONTAL)
112 self.mute = Gtk.ToggleButton()
113 self.mute.set_label(_("M"))
114 self.mute.get_style_context().add_class("mute")
115 self.mute.set_active(self.channel.out_mute)
116 self.mute.connect("toggled", self.on_mute_toggled)
117 self.mute.connect("button-press-event", self.on_mute_button_pressed)
118 self.hbox_mutesolo.pack_start(self.mute, True, True, 0)
120 self.pack_start(self.hbox_mutesolo, False, False, 0)
122 self.monitor_button = Gtk.ToggleButton(_("MON"))
123 self.monitor_button.get_style_context().add_class("monitor")
124 self.monitor_button.connect("toggled", self.on_monitor_button_toggled)
125 self.pack_start(self.monitor_button, False, False, 0)
127 def create_fader(self):
128 # HBox for fader and meter
129 self.vbox_fader = Gtk.Box(orientation=Gtk.Orientation.VERTICAL)
130 self.vbox_fader.get_style_context().add_class("vbox_fader")
132 self.hbox_readouts = Gtk.Box(orientation=Gtk.Orientation.HORIZONTAL)
133 self.hbox_readouts.set_homogeneous(True)
134 self.hbox_readouts.pack_start(self.volume_digits, False, True, 0)
135 self.hbox_readouts.pack_start(self.abspeak, False, True, 0)
136 self.vbox_fader.pack_start(self.hbox_readouts, False, False, 0)
138 self.hbox_fader = Gtk.Box(orientation=Gtk.Orientation.HORIZONTAL)
139 self.hbox_fader.pack_start(self.slider, True, True, 0)
140 self.hbox_fader.pack_start(self.meter, True, True, 0)
141 self.vbox_fader.pack_start(self.hbox_fader, True, True, 0)
142 self.vbox_fader.pack_end(self.balance, False, True, 0)
144 self.pack_start(self.vbox_fader, True, True, 0)
146 def create_slider_widget(self):
147 parent = None
148 if self.slider:
149 parent = self.slider.get_parent()
150 self.slider.destroy()
152 if self.gui_factory.use_custom_widgets:
153 self.slider = slider.CustomSliderWidget(self.slider_adjustment)
154 else:
155 self.slider = slider.VolumeSlider(self.slider_adjustment)
157 if parent:
158 parent.pack_start(self.slider, True, True, 0)
159 parent.reorder_child(self.slider, 0)
161 self.slider.show()
163 def realize(self):
164 log.debug('Realizing channel "%s".', self.channel_name)
165 if self.future_out_mute is not None:
166 self.channel.out_mute = self.future_out_mute
168 # Widgets
169 # Channel strip label
170 self.vbox = Gtk.Box(orientation=Gtk.Orientation.VERTICAL)
171 self.pack_start(self.vbox, False, True, 0)
172 self.label_name = Gtk.Label()
173 self.label_name.get_style_context().add_class("top_label")
174 self.label_name.set_text(self.channel_name)
175 self.label_name.set_max_width_chars(
176 self.label_chars_wide if self.wide else self.label_chars_narrow
178 self.label_name.set_ellipsize(Pango.EllipsizeMode.MIDDLE)
179 self.label_name_event_box = Gtk.EventBox()
180 self.label_name_event_box.connect("button-press-event", self.on_label_mouse)
181 self.label_name_event_box.add(self.label_name)
183 # Volume slider
184 self.slider = None
185 self.create_slider_widget()
186 # Balance slider
187 self.balance = None
188 self.create_balance_widget()
190 # Volume entry
191 self.volume_digits = Gtk.Entry()
192 self.volume_digits.set_has_frame(False)
193 self.volume_digits.set_width_chars(5)
194 self.volume_digits.set_property("xalign", 0.5)
195 self.volume_digits.connect("key-press-event", self.on_volume_digits_key_pressed)
196 self.volume_digits.connect("focus-out-event", self.on_volume_digits_focus_out)
197 self.volume_digits.get_style_context().add_class("readout")
199 # Peak level label
200 self.abspeak = abspeak.AbspeakWidget()
201 self.abspeak.connect("reset", self.on_abspeak_reset)
202 self.abspeak.connect("volume-adjust", self.on_abspeak_adjust)
203 self.abspeak.get_style_context().add_class("readout")
205 # Level meter
206 if self.stereo:
207 self.meter = meter.StereoMeterWidget(self.meter_scale)
208 else:
209 self.meter = meter.MonoMeterWidget(self.meter_scale)
211 self.meter.set_events(Gdk.EventMask.SCROLL_MASK)
212 self.on_vumeter_color_changed(self.gui_factory)
214 # If channel is created via UI, the initial volume is passed to the
215 # init method and saved in the `initial_vol` attribute.
216 # If channel is created from a project XML file, no initial volume is
217 # passsed to init, `initial_vol` will be `None` and the volume slider
218 # adjustment is set via the `unserialize_property` method.
219 # In both cases the engine volume (and balance) will be synchronized
220 # with the slider adjustment by the overwritten `realize` method in
221 # Channel sub-classes.
222 if self.initial_vol is not None:
223 if self.initial_vol == -1:
224 self.slider_adjustment.set_value(0)
225 else:
226 self.slider_adjustment.set_value_db(self.initial_vol)
228 self.slider_adjustment.connect("volume-changed", self.on_volume_changed)
229 self.slider_adjustment.connect(
230 "volume-changed-from-midi", self.on_volume_changed_from_midi
232 self.balance_adjustment.connect("balance-changed", self.on_balance_changed)
234 self.gui_factory.connect(
235 "default-meter-scale-changed", self.on_default_meter_scale_changed
237 self.gui_factory.connect(
238 "default-slider-scale-changed", self.on_default_slider_scale_changed
240 self.gui_factory.connect("vumeter-color-changed", self.on_vumeter_color_changed)
241 self.gui_factory.connect("vumeter-color-scheme-changed", self.on_vumeter_color_changed)
242 self.gui_factory.connect("use-custom-widgets-changed", self.on_custom_widgets_changed)
244 self.connect("key-press-event", self.on_key_pressed)
245 self.connect("scroll-event", self.on_scroll)
247 entries = [Gtk.TargetEntry.new(self.__class__.__name__, Gtk.TargetFlags.SAME_APP, 0)]
248 self.label_name_event_box.drag_source_set(
249 Gdk.ModifierType.BUTTON1_MASK, entries, Gdk.DragAction.MOVE
251 self.label_name_event_box.connect("drag-data-get", self.on_drag_data_get)
252 self.drag_dest_set(Gtk.DestDefaults.ALL, entries, Gdk.DragAction.MOVE)
253 self.connect_after("drag-data-received", self.on_drag_data_received)
255 self.vbox.pack_start(self.label_name_event_box, True, True, 0)
257 def unrealize(self):
258 log.debug('Unrealizing channel "%s".', self.channel_name)
260 # ---------------------------------------------------------------------------------------------
261 # Signal/event handlers
263 def on_label_mouse(self, widget, event):
264 if event.type == Gdk.EventType._2BUTTON_PRESS:
265 if event.button == 1:
266 self.on_channel_properties()
267 return True
268 elif (
269 event.state & Gdk.ModifierType.CONTROL_MASK
270 and event.type == Gdk.EventType.BUTTON_PRESS
271 and event.button == 1
273 if self.wide:
274 self.narrow()
275 else:
276 self.widen()
277 return True
279 def on_channel_properties(self):
280 if not self.channel_properties_dialog:
281 self.channel_properties_dialog = self.properties_dialog_class(self.app, self)
282 self.channel_properties_dialog.fill_and_show()
284 def on_default_meter_scale_changed(self, gui_factory, scale):
285 log.debug("Default meter scale change detected.")
286 self.meter.set_scale(scale)
288 def on_default_slider_scale_changed(self, gui_factory, scale):
289 log.debug("Default slider scale change detected.")
290 self.slider_scale = scale
291 self.slider_adjustment.set_scale(scale)
292 if self.channel:
293 self.channel.midi_scale = self.slider_scale.scale
295 def on_vumeter_color_changed(self, gui_factory, *args):
296 color = gui_factory.get_vumeter_color()
297 color_scheme = gui_factory.get_vumeter_color_scheme()
298 if color_scheme != "solid":
299 self.meter.set_color(None)
300 else:
301 self.meter.set_color(Gdk.color_parse(color))
303 def on_custom_widgets_changed(self, gui_factory, value):
304 self.create_slider_widget()
305 # balance slider has no custom variant, no need to re-create it.
307 def on_abspeak_adjust(self, abspeak, adjust):
308 log.debug("abspeak adjust %f", adjust)
309 self.slider_adjustment.set_value_db(self.slider_adjustment.get_value_db() + adjust)
310 self.channel.abspeak = None
311 # We want to update gui even if actual decibels have not changed (scale wrap for example)
312 # self.update_volume(False)
314 def on_abspeak_reset(self, abspeak):
315 log.debug("abspeak reset")
316 self.channel.abspeak = None
318 def on_volume_digits_key_pressed(self, widget, event):
319 if event.keyval == Gdk.KEY_Return or event.keyval == Gdk.KEY_KP_Enter:
320 db_text = self.volume_digits.get_text()
321 try:
322 db = float(db_text)
323 log.debug("Volume digits confirmation '%f dBFS'.", db)
324 except ValueError:
325 log.debug("Volume digits confirmation ignore, reset to current.")
326 self.update_volume(False)
327 return
328 self.slider_adjustment.set_value_db(db)
329 # self.grab_focus()
330 # We want to update gui even if actual decibels have not changed
331 # (scale wrap for example)
332 # self.update_volume(False)
334 def on_volume_digits_focus_out(self, widget, event):
335 log.debug("Volume digits focus out detected.")
336 self.update_volume(False)
338 def on_scroll(self, widget, event):
339 if event.direction == Gdk.ScrollDirection.DOWN:
340 self.slider_adjustment.step_down()
341 elif event.direction == Gdk.ScrollDirection.UP:
342 self.slider_adjustment.step_up()
343 return True
345 def on_volume_changed(self, adjustment):
346 self.update_volume(True)
348 def on_volume_changed_from_midi(self, adjustment):
349 self.update_volume(True, from_midi=True)
351 def on_balance_changed(self, adjustment):
352 self.update_balance(True)
354 def on_key_pressed(self, widget, event):
355 if event.keyval == Gdk.KEY_Up:
356 log.debug(self.channel_name + " Up")
357 self.slider_adjustment.step_up()
358 return True
359 elif event.keyval == Gdk.KEY_Down:
360 log.debug(self.channel_name + " Down")
361 self.slider_adjustment.step_down()
362 return True
364 return False
366 def on_drag_data_get(self, widget, drag_context, data, info, time):
367 data.set(data.get_target(), 8, self.channel_name.encode("utf-8"))
369 def on_drag_data_received(self, widget, drag_context, x, y, data, info, time):
370 pass
372 def on_midi_event_received(self, *args):
373 self.slider_adjustment.set_value_db(self.channel.volume, from_midi=True)
374 self.balance_adjustment.set_balance(self.channel.balance, from_midi=True)
376 def on_mute_toggled(self, button):
377 self.channel.out_mute = self.mute.get_active()
379 def on_mute_button_pressed(self, button, event, *args):
380 # should be overwritten by sub-class
381 pass
383 def on_monitor_button_toggled(self, button):
384 if button.get_active():
385 for channel in self.app.channels + self.app.output_channels:
386 if channel is not self and channel.monitor_button.get_active():
387 channel.monitor_button.handler_block_by_func(channel.on_monitor_button_toggled)
388 channel.monitor_button.set_active(False)
389 channel.monitor_button.handler_unblock_by_func(
390 channel.on_monitor_button_toggled
392 self.app.monitored_channel = self
393 else:
394 if self.app.monitored_channel is self:
395 self.app.monitored_channel = None
397 def assign_midi_ccs(self, volume_cc, balance_cc, mute_cc, solo_cc=None):
398 try:
399 if volume_cc != -1:
400 self.channel.volume_midi_cc = volume_cc
401 else:
402 volume_cc = self.channel.autoset_volume_midi_cc()
404 log.debug("Channel '%s' volume assigned to CC #%s.", self.channel.name, volume_cc)
405 except Exception as exc:
406 log.error("Channel '%s' volume CC assignment failed: %s", self.channel.name, exc)
408 try:
409 if balance_cc != -1:
410 self.channel.balance_midi_cc = balance_cc
411 else:
412 balance_cc = self.channel.autoset_balance_midi_cc()
414 log.debug("Channel '%s' balance assigned to CC #%s.", self.channel.name, balance_cc)
415 except Exception as exc:
416 log.error("Channel '%s' balance CC assignment failed: %s", self.channel.name, exc)
418 try:
419 if mute_cc != -1:
420 self.channel.mute_midi_cc = mute_cc
421 else:
422 mute_cc = self.channel.autoset_mute_midi_cc()
424 log.debug("Channel '%s' mute assigned to CC #%s.", self.channel.name, mute_cc)
425 except Exception as exc:
426 log.error("Channel '%s' mute CC assignment failed: %s", self.channel.name, exc)
428 if solo_cc is not None:
429 try:
430 if solo_cc != -1:
431 self.channel.solo_midi_cc = solo_cc
432 else:
433 solo_cc = self.channel.autoset_solo_midi_cc()
435 log.debug("Channel '%s' solo assigned to CC #%s.", self.channel.name, solo_cc)
436 except Exception as exc:
437 log.error("Channel '%s' solo CC assignment failed: %s", self.channel.name, exc)
439 # ---------------------------------------------------------------------------------------------
440 # Channel operations
442 def set_monitored(self):
443 if self.channel:
444 self.app.set_monitored_channel(self)
445 self.monitor_button.set_active(True)
447 def set_color(self, color):
448 self.color = color
449 set_background_color(self.label_name_event_box, self.css_name, self.color)
451 def widen(self, flag=True):
452 self.wide = flag
453 ctx = self.label_name.get_style_context()
455 if flag:
456 ctx.remove_class("narrow")
457 ctx.add_class("wide")
458 else:
459 ctx.remove_class("wide")
460 ctx.add_class("narrow")
462 label = self.label_name.get_label()
463 label_width = self.label_chars_wide if flag else self.label_chars_narrow
464 self.label_name.set_max_width_chars(label_width)
466 if len(label) > label_width:
467 self.label_name.set_tooltip_text(label)
469 self.meter.widen(flag)
470 self.hbox_readouts.set_orientation(
471 Gtk.Orientation.HORIZONTAL if flag else Gtk.Orientation.VERTICAL
474 def narrow(self):
475 self.widen(False)
477 def read_meter(self):
478 if not self.channel:
479 return
480 if self.stereo:
481 peak_left, peak_right, rms_left, rms_right = self.channel.kmeter
482 self.meter.set_values(peak_left, peak_right, rms_left, rms_right)
483 else:
484 peak, rms = self.channel.kmeter
485 self.meter.set_values(peak, rms)
487 self.abspeak.set_peak(self.channel.abspeak)
489 def update_balance(self, update_engine, from_midi=False):
490 balance = self.balance_adjustment.get_value()
491 log.debug("%s balance: %f", self.channel_name, balance)
493 if update_engine:
494 if not from_midi:
495 self.channel.balance = balance
496 self.channel.set_midi_cc_balance_picked_up(False)
497 self.app.update_monitor(self)
499 def update_volume(self, update_engine, from_midi=False):
500 db = self.slider_adjustment.get_value_db()
501 log.debug("%s volume: %.2f dB", self.channel_name, db)
503 # TODO l10n
504 db_text = "%.2f" % db
505 self.volume_digits.set_text(db_text)
507 if update_engine:
508 if not from_midi:
509 self.channel.volume = db
510 self.channel.set_midi_cc_volume_picked_up(False)
511 self.app.update_monitor(self)
513 # ---------------------------------------------------------------------------------------------
514 # Channel (de-)serialization
516 def serialize(self, object_backend):
517 object_backend.add_property("volume", "%f" % self.slider_adjustment.get_value_db())
518 object_backend.add_property("balance", "%f" % self.balance_adjustment.get_value())
519 object_backend.add_property("wide", "%s" % str(self.wide))
521 if hasattr(self.channel, "out_mute"):
522 object_backend.add_property("out_mute", str(self.channel.out_mute))
523 if self.channel.volume_midi_cc != -1:
524 object_backend.add_property("volume_midi_cc", str(self.channel.volume_midi_cc))
525 if self.channel.balance_midi_cc != -1:
526 object_backend.add_property("balance_midi_cc", str(self.channel.balance_midi_cc))
527 if self.channel.mute_midi_cc != -1:
528 object_backend.add_property("mute_midi_cc", str(self.channel.mute_midi_cc))
529 if self.channel.solo_midi_cc != -1:
530 object_backend.add_property("solo_midi_cc", str(self.channel.solo_midi_cc))
532 def unserialize_property(self, name, value):
533 if name == "volume":
534 self.slider_adjustment.set_value_db(float(value))
535 return True
536 elif name == "balance":
537 self.balance_adjustment.set_value(float(value))
538 return True
539 elif name == "out_mute":
540 self.future_out_mute = value == "True"
541 return True
542 elif name == "volume_midi_cc":
543 self.future_volume_midi_cc = int(value)
544 return True
545 elif name == "balance_midi_cc":
546 self.future_balance_midi_cc = int(value)
547 return True
548 elif name == "mute_midi_cc":
549 self.future_mute_midi_cc = int(value)
550 return True
551 elif name == "solo_midi_cc":
552 self.future_solo_midi_cc = int(value)
553 return True
554 elif name == "wide":
555 self.wide = value == "True"
556 return True
558 return False
561 class InputChannel(Channel):
562 def __init__(self, *args, **kwargs):
563 super().__init__(*args, **kwargs)
564 self.properties_dialog_class = ChannelPropertiesDialog
566 def create_buttons(self):
567 super().create_buttons()
568 self.solo = Gtk.ToggleButton()
569 self.solo.set_label(_("S"))
570 self.solo.get_style_context().add_class("solo")
571 self.solo.set_active(self.channel.solo)
572 self.solo.connect("toggled", self.on_solo_toggled)
573 self.solo.connect("button-press-event", self.on_solo_button_pressed)
574 self.hbox_mutesolo.pack_start(self.solo, True, True, 0)
576 def realize(self):
577 self.channel = self.mixer.add_channel(self.channel_name, self.stereo)
579 if self.channel is None:
580 raise Exception(_("Cannot create a channel"))
582 super().realize()
584 if self.future_volume_midi_cc is not None:
585 self.channel.volume_midi_cc = self.future_volume_midi_cc
586 if self.future_balance_midi_cc is not None:
587 self.channel.balance_midi_cc = self.future_balance_midi_cc
588 if self.future_mute_midi_cc is not None:
589 self.channel.mute_midi_cc = self.future_mute_midi_cc
590 if self.future_solo_midi_cc is not None:
591 self.channel.solo_midi_cc = self.future_solo_midi_cc
592 if self.app._init_solo_channels and self.channel_name in self.app._init_solo_channels:
593 self.channel.solo = True
595 self.channel.midi_scale = self.slider_scale.scale
597 self.update_volume(update_engine=True)
598 self.update_balance(update_engine=True)
600 self.create_fader()
601 self.create_buttons()
603 if not self.wide:
604 self.narrow()
606 def unrealize(self):
607 super().unrealize()
608 if self.post_fader_output_channel:
609 self.post_fader_output_channel.remove()
610 self.post_fader_output_channel = None
611 self.channel.remove()
612 self.channel = None
614 def narrow(self):
615 super().narrow()
616 for cg in self.get_control_groups():
617 cg.narrow()
619 def widen(self, flag=True):
620 super().widen(flag)
621 for cg in self.get_control_groups():
622 cg.widen()
624 def on_drag_data_received(self, widget, drag_context, x, y, data, info, time):
625 source_name = data.get_data().decode("utf-8")
626 if source_name == self.channel_name:
627 return
628 self.emit("input-channel-order-changed", source_name, self.channel_name)
630 def add_control_group(self, channel):
631 control_group = ControlGroup(channel, self)
632 control_group.show_all()
633 self.vbox.pack_start(control_group, True, True, 0)
634 return control_group
636 def remove_control_group(self, channel):
637 ctlgroup = self.get_control_group(channel)
638 self.vbox.remove(ctlgroup)
640 def update_control_group(self, channel):
641 for control_group in self.vbox.get_children():
642 if isinstance(control_group, ControlGroup):
643 if control_group.output_channel is channel:
644 control_group.update()
646 def get_control_group(self, channel):
647 for control_group in self.get_control_groups():
648 if control_group.output_channel is channel:
649 return control_group
650 return None
652 def get_control_groups(self):
653 ctlgroups = []
654 for c in self.vbox.get_children():
655 if isinstance(c, ControlGroup):
656 ctlgroups.append(c)
657 return ctlgroups
659 def midi_events_check(self):
660 if self.channel is not None and self.channel.midi_in_got_events:
661 self.mute.set_active(self.channel.out_mute)
662 self.solo.set_active(self.channel.solo)
663 super().on_midi_event_received()
665 def on_mute_button_pressed(self, button, event, *args):
666 if event.button == 1 and event.state & Gdk.ModifierType.CONTROL_MASK:
667 # Ctrlr+left-click: exclusive (between input channels) mute
668 for channel in self.app.channels:
669 if channel is not self:
670 channel.mute.set_active(False)
671 return button.get_active()
672 elif event.button == 3:
673 # Right-click on the mute button: act on all output channels
674 if button.get_active(): # was muted
675 button.set_active(False)
676 if hasattr(button, "touched_channels"):
677 touched_channels = button.touched_channels
678 for chan in touched_channels:
679 ctlgroup = self.get_control_group(chan)
680 ctlgroup.mute.set_active(False)
681 del button.touched_channels
682 else: # was not muted
683 button.set_active(True)
684 touched_channels = []
685 for chan in self.app.output_channels:
686 ctlgroup = self.get_control_group(chan)
687 if not ctlgroup.mute.get_active():
688 ctlgroup.mute.set_active(True)
689 touched_channels.append(chan)
690 button.touched_channels = touched_channels
691 return True
692 return False
694 def on_solo_toggled(self, button):
695 self.channel.solo = self.solo.get_active()
697 def on_solo_button_pressed(self, button, event, *args):
698 if event.button == 1 and event.state & Gdk.ModifierType.CONTROL_MASK:
699 # Ctrlr+left-click: exclusive solo
700 for channel in self.app.channels:
701 if channel is not self:
702 channel.solo.set_active(False)
703 return button.get_active()
704 elif event.button == 3:
705 # Right click on the solo button: act on all output channels
706 if button.get_active(): # was soloed
707 button.set_active(False)
708 if hasattr(button, "touched_channels"):
709 touched_channels = button.touched_channels
710 for chan in touched_channels:
711 ctlgroup = self.get_control_group(chan)
712 ctlgroup.solo.set_active(False)
713 del button.touched_channels
714 else: # was not soloed
715 button.set_active(True)
716 touched_channels = []
717 for chan in self.app.output_channels:
718 ctlgroup = self.get_control_group(chan)
719 if not ctlgroup.solo.get_active():
720 ctlgroup.solo.set_active(True)
721 touched_channels.append(chan)
722 button.touched_channels = touched_channels
723 return True
724 return False
726 @classmethod
727 def serialization_name(cls):
728 return "input_channel"
730 def serialize(self, object_backend):
731 object_backend.add_property("name", self.channel_name)
732 if self.stereo:
733 object_backend.add_property("type", "stereo")
734 else:
735 object_backend.add_property("type", "mono")
736 object_backend.add_property("direct_output", "%s" % str(self.wants_direct_output))
737 super().serialize(object_backend)
739 def unserialize_property(self, name, value):
740 if name == "name":
741 self.channel_name = str(value)
742 return True
743 elif name == "type":
744 if value == "stereo":
745 self.stereo = True
746 return True
747 elif value == "mono":
748 self.stereo = False
749 return True
750 elif name == "direct_output":
751 self.wants_direct_output = value == "True"
752 return True
754 return super().unserialize_property(name, value)
757 class OutputChannel(Channel):
758 def __init__(self, *args, **kwargs):
759 super().__init__(*args, **kwargs)
760 self.properties_dialog_class = OutputChannelPropertiesDialog
761 self._display_solo_buttons = False
762 self._init_muted_channels = None
763 self._init_solo_channels = None
764 self._init_prefader_channels = None
765 self._color = Gdk.RGBA()
767 @property
768 def display_solo_buttons(self):
769 return self._display_solo_buttons
771 @display_solo_buttons.setter
772 def display_solo_buttons(self, value):
773 self._display_solo_buttons = value
774 # notifying control groups
775 for inputchannel in self.app.channels:
776 inputchannel.update_control_group(self)
778 @property
779 def color(self):
780 return self._color
782 @color.setter
783 def color(self, value):
784 if isinstance(value, Gdk.RGBA):
785 self._color = value
786 else:
787 c = Gdk.RGBA()
788 c.parse(value)
789 self._color = c
791 def realize(self):
792 self.channel = self.mixer.add_output_channel(self.channel_name, self.stereo)
794 if self.channel is None:
795 raise Exception(_("Cannot create an output channel"))
797 super().realize()
799 if self.future_volume_midi_cc is not None:
800 self.channel.volume_midi_cc = self.future_volume_midi_cc
801 if self.future_balance_midi_cc is not None:
802 self.channel.balance_midi_cc = self.future_balance_midi_cc
803 if self.future_mute_midi_cc is not None:
804 self.channel.mute_midi_cc = self.future_mute_midi_cc
806 self.channel.midi_scale = self.slider_scale.scale
808 self.update_volume(update_engine=True)
809 self.update_balance(update_engine=True)
811 set_background_color(self.label_name_event_box, self.css_name, self.color)
813 self.create_fader()
814 self.create_buttons()
816 # add control groups to the input channels, and initialize them
817 # appropriately
818 for input_channel in self.app.channels:
819 ctlgroup = input_channel.add_control_group(self)
820 name = input_channel.channel.name
821 if self._init_muted_channels and name in self._init_muted_channels:
822 ctlgroup.mute.set_active(True)
823 if self._init_solo_channels and name in self._init_solo_channels:
824 ctlgroup.solo.set_active(True)
825 if self._init_prefader_channels and name in self._init_prefader_channels:
826 ctlgroup.prefader.set_active(True)
827 if not input_channel.wide:
828 ctlgroup.narrow()
830 self._init_muted_channels = None
831 self._init_solo_channels = None
832 self._init_prefader_channels = None
834 if not self.wide:
835 self.narrow()
837 def unrealize(self):
838 # remove control groups from input channels
839 for input_channel in self.app.channels:
840 input_channel.remove_control_group(self)
841 # then remove itself
842 super().unrealize()
843 self.channel.remove()
844 self.channel = None
846 def on_drag_data_received(self, widget, drag_context, x, y, data, info, time):
847 source_name = data.get_data().decode("utf-8")
848 if source_name == self.channel_name:
849 return
850 self.emit("output-channel-order-changed", source_name, self.channel_name)
852 def on_mute_button_pressed(self, button, event, *args):
853 if event.button == 1 and event.state & Gdk.ModifierType.CONTROL_MASK:
854 # Ctrlr+left-click: exclusive (between output channels) mute
855 for channel in self.app.output_channels:
856 if channel is not self:
857 channel.mute.set_active(False)
858 return button.get_active()
860 def midi_events_check(self):
861 if self.channel is not None and self.channel.midi_in_got_events:
862 self.mute.set_active(self.channel.out_mute)
863 super().on_midi_event_received()
865 @classmethod
866 def serialization_name(cls):
867 return "output_channel"
869 def serialize(self, object_backend):
870 object_backend.add_property("name", self.channel_name)
871 if self.stereo:
872 object_backend.add_property("type", "stereo")
873 else:
874 object_backend.add_property("type", "mono")
875 if self.display_solo_buttons:
876 object_backend.add_property("solo_buttons", "true")
877 muted_channels = []
878 solo_channels = []
879 prefader_in_channels = []
880 for input_channel in self.app.channels:
881 if self.channel.is_muted(input_channel.channel):
882 muted_channels.append(input_channel)
883 if self.channel.is_solo(input_channel.channel):
884 solo_channels.append(input_channel)
885 if self.channel.is_in_prefader(input_channel.channel):
886 prefader_in_channels.append(input_channel)
887 if muted_channels:
888 object_backend.add_property(
889 "muted_channels", "|".join([x.channel.name for x in muted_channels])
891 if solo_channels:
892 object_backend.add_property(
893 "solo_channels", "|".join([x.channel.name for x in solo_channels])
895 if prefader_in_channels:
896 object_backend.add_property(
897 "prefader_channels", "|".join([x.channel.name for x in prefader_in_channels])
899 object_backend.add_property("color", self.color.to_string())
900 super().serialize(object_backend)
902 def unserialize_property(self, name, value):
903 if name == "name":
904 self.channel_name = str(value)
905 return True
906 elif name == "type":
907 if value == "stereo":
908 self.stereo = True
909 return True
910 elif value == "mono":
911 self.stereo = False
912 return True
913 elif name == "solo_buttons":
914 if value == "true":
915 self.display_solo_buttons = True
916 return True
917 elif name == "muted_channels":
918 self._init_muted_channels = value.split("|")
919 return True
920 elif name == "solo_channels":
921 self._init_solo_channels = value.split("|")
922 return True
923 elif name == "prefader_channels":
924 self._init_prefader_channels = value.split("|")
925 return True
926 elif name == "color":
927 c = Gdk.RGBA()
928 c.parse(value)
929 self.color = c
930 return True
932 return super().unserialize_property(name, value)
935 class ChannelPropertiesDialog(Gtk.Dialog):
936 def __init__(self, app, channel=None, title=None):
937 if not title:
938 if not channel:
939 raise ValueError("Either 'title' or 'channel' must be passed.")
940 title = _("Channel '{name}' Properties").format(name=channel.channel_name)
942 super().__init__(title, app.window)
943 self.channel = channel
944 self.app = app
945 self.mixer = app.mixer
946 self.set_default_size(365, -1)
948 self.create_ui()
950 def fill_and_show(self):
951 self.fill_ui()
952 self.present()
954 def add_buttons(self):
955 self.add_button(Gtk.STOCK_CANCEL, Gtk.ResponseType.CANCEL)
956 self.ok_button = self.add_button(Gtk.STOCK_APPLY, Gtk.ResponseType.APPLY)
957 self.set_default_response(Gtk.ResponseType.APPLY)
959 self.connect("response", self.on_response_cb)
960 self.connect("delete-event", self.on_response_cb)
962 def create_frame(self, label, child, padding=8):
963 # need to pass an empty label, otherwise no label widget is created
964 frame = Gtk.Frame(label="")
965 frame.get_label_widget().set_markup("<b>%s</b>" % label)
966 frame.set_border_width(3)
967 frame.set_shadow_type(Gtk.ShadowType.NONE)
969 alignment = Gtk.Alignment.new(0.5, 0, 1, 1)
970 alignment.set_padding(padding, padding, padding, padding)
971 frame.add(alignment)
972 alignment.add(child)
974 return frame
976 def create_ui(self):
977 self.add_buttons()
978 vbox = self.get_content_area()
980 self.properties_grid = grid = Gtk.Grid()
981 vbox.pack_start(self.create_frame(_("Properties"), grid), True, True, 0)
982 grid.set_row_spacing(8)
983 grid.set_column_spacing(8)
984 grid.set_column_homogeneous(True)
986 name_label = Gtk.Label.new_with_mnemonic(_("_Name"))
987 name_label.set_halign(Gtk.Align.START)
988 grid.attach(name_label, 0, 0, 1, 1)
989 self.entry_name = Gtk.Entry()
990 self.entry_name.set_activates_default(True)
991 self.entry_name.connect("changed", self.on_entry_name_changed)
992 name_label.set_mnemonic_widget(self.entry_name)
993 grid.attach(self.entry_name, 1, 0, 2, 1)
995 grid.attach(Gtk.Label(label=_("Mode"), halign=Gtk.Align.START), 0, 1, 1, 1)
996 self.mono = Gtk.RadioButton.new_with_mnemonic(None, _("_Mono"))
997 self.stereo = Gtk.RadioButton.new_with_mnemonic_from_widget(self.mono, _("_Stereo"))
998 grid.attach(self.mono, 1, 1, 1, 1)
999 grid.attach(self.stereo, 2, 1, 1, 1)
1001 grid = Gtk.Grid()
1002 vbox.pack_start(self.create_frame(_("MIDI Control Changes"), grid), True, True, 0)
1003 grid.set_row_spacing(8)
1004 grid.set_column_spacing(8)
1005 grid.set_column_homogeneous(True)
1007 cc_tooltip = _(
1008 "{param} MIDI Control Change number " "(0-127, set to -1 to assign next free CC #)"
1010 volume_label = Gtk.Label.new_with_mnemonic(_("_Volume"))
1011 volume_label.set_halign(Gtk.Align.START)
1012 grid.attach(volume_label, 0, 0, 1, 1)
1013 self.entry_volume_cc = Gtk.SpinButton.new_with_range(-1, 127, 1)
1014 self.entry_volume_cc.set_tooltip_text(cc_tooltip.format(param=_("Volume")))
1015 volume_label.set_mnemonic_widget(self.entry_volume_cc)
1016 grid.attach(self.entry_volume_cc, 1, 0, 1, 1)
1017 self.button_sense_midi_volume = Gtk.Button(_("Learn"))
1018 self.button_sense_midi_volume.connect("clicked", self.on_sense_midi_volume_clicked)
1019 grid.attach(self.button_sense_midi_volume, 2, 0, 1, 1)
1021 balance_label = Gtk.Label.new_with_mnemonic(_("_Balance"))
1022 balance_label.set_halign(Gtk.Align.START)
1023 grid.attach(balance_label, 0, 1, 1, 1)
1024 self.entry_balance_cc = Gtk.SpinButton.new_with_range(-1, 127, 1)
1025 self.entry_balance_cc.set_tooltip_text(cc_tooltip.format(param=_("Balance")))
1026 balance_label.set_mnemonic_widget(self.entry_balance_cc)
1027 grid.attach(self.entry_balance_cc, 1, 1, 1, 1)
1028 self.button_sense_midi_balance = Gtk.Button(_("Learn"))
1029 self.button_sense_midi_balance.connect("clicked", self.on_sense_midi_balance_clicked)
1030 grid.attach(self.button_sense_midi_balance, 2, 1, 1, 1)
1032 mute_label = Gtk.Label.new_with_mnemonic(_("M_ute"))
1033 mute_label.set_halign(Gtk.Align.START)
1034 grid.attach(mute_label, 0, 2, 1, 1)
1035 self.entry_mute_cc = Gtk.SpinButton.new_with_range(-1, 127, 1)
1036 self.entry_mute_cc.set_tooltip_text(cc_tooltip.format(param=_("Mute")))
1037 mute_label.set_mnemonic_widget(self.entry_mute_cc)
1038 grid.attach(self.entry_mute_cc, 1, 2, 1, 1)
1039 self.button_sense_midi_mute = Gtk.Button(_("Learn"))
1040 self.button_sense_midi_mute.connect("clicked", self.on_sense_midi_mute_clicked)
1041 grid.attach(self.button_sense_midi_mute, 2, 2, 1, 1)
1043 if isinstance(self, NewInputChannelDialog) or (
1044 self.channel and isinstance(self.channel, InputChannel)
1046 direct_output_label = Gtk.Label.new_with_mnemonic(_("_Direct Out(s)"))
1047 direct_output_label.set_halign(Gtk.Align.START)
1048 self.properties_grid.attach(direct_output_label, 0, 3, 1, 1)
1049 self.direct_output = Gtk.CheckButton()
1050 direct_output_label.set_mnemonic_widget(self.direct_output)
1051 self.direct_output.set_tooltip_text(_("Add direct post-fader output(s) for channel."))
1052 self.properties_grid.attach(self.direct_output, 1, 3, 1, 1)
1054 if isinstance(self, NewChannelDialog) or (
1055 self.channel and isinstance(self.channel, InputChannel)
1057 solo_label = Gtk.Label.new_with_mnemonic(_("S_olo"))
1058 solo_label.set_halign(Gtk.Align.START)
1059 grid.attach(solo_label, 0, 3, 1, 1)
1060 self.entry_solo_cc = Gtk.SpinButton.new_with_range(-1, 127, 1)
1061 self.entry_solo_cc.set_tooltip_text(cc_tooltip.format(param=_("Solo")))
1062 solo_label.set_mnemonic_widget(self.entry_solo_cc)
1063 grid.attach(self.entry_solo_cc, 1, 3, 1, 1)
1064 self.button_sense_midi_solo = Gtk.Button(_("Learn"))
1065 self.button_sense_midi_solo.connect("clicked", self.on_sense_midi_solo_clicked)
1066 grid.attach(self.button_sense_midi_solo, 2, 3, 1, 1)
1068 self.vbox.show_all()
1070 def fill_ui(self):
1071 self.entry_name.set_text(self.channel.channel_name)
1072 if self.channel.channel.is_stereo:
1073 self.stereo.set_active(True)
1074 else:
1075 self.mono.set_active(True)
1076 self.mono.set_sensitive(False)
1077 self.stereo.set_sensitive(False)
1078 self.entry_volume_cc.set_value(self.channel.channel.volume_midi_cc)
1079 self.entry_balance_cc.set_value(self.channel.channel.balance_midi_cc)
1080 self.entry_mute_cc.set_value(self.channel.channel.mute_midi_cc)
1081 if self.channel and isinstance(self.channel, InputChannel):
1082 self.direct_output.set_active(self.channel.wants_direct_output)
1083 self.entry_solo_cc.set_value(self.channel.channel.solo_midi_cc)
1085 def sense_popup_dialog(self, entry):
1086 window = Gtk.Window.new(Gtk.WindowType.TOPLEVEL)
1087 window.set_destroy_with_parent(True)
1088 window.set_transient_for(self)
1089 window.set_decorated(False)
1090 window.set_modal(True)
1091 window.set_position(Gtk.WindowPosition.CENTER_ON_PARENT)
1092 window.set_border_width(10)
1094 vbox = Gtk.Box(10, orientation=Gtk.Orientation.VERTICAL)
1095 window.add(vbox)
1096 window.timeout = 5
1097 label = Gtk.Label(
1098 label=_("Please move the MIDI control you want to use for this function.")
1100 vbox.pack_start(label, True, True, 0)
1101 timeout_label = Gtk.Label(label=_("This window will close in 5 seconds."))
1102 vbox.pack_start(timeout_label, True, True, 0)
1104 def close_sense_timeout(window, entry):
1105 window.timeout -= 1
1106 timeout_label.set_text(
1107 _("This window will close in {seconds} seconds.").format(seconds=window.timeout)
1109 if window.timeout == 0:
1110 window.destroy()
1111 entry.set_value(self.mixer.last_midi_cc)
1112 return False
1113 return True
1115 window.show_all()
1116 GObject.timeout_add_seconds(1, close_sense_timeout, window, entry)
1118 def on_sense_midi_volume_clicked(self, *args):
1119 self.mixer.last_midi_cc = int(self.entry_volume_cc.get_value())
1120 self.sense_popup_dialog(self.entry_volume_cc)
1122 def on_sense_midi_balance_clicked(self, *args):
1123 self.mixer.last_midi_cc = int(self.entry_balance_cc.get_value())
1124 self.sense_popup_dialog(self.entry_balance_cc)
1126 def on_sense_midi_mute_clicked(self, *args):
1127 self.mixer.last_midi_cc = int(self.entry_mute_cc.get_value())
1128 self.sense_popup_dialog(self.entry_mute_cc)
1130 def on_sense_midi_solo_clicked(self, *args):
1131 self.mixer.last_midi_cc = int(self.entry_solo_cc.get_value())
1132 self.sense_popup_dialog(self.entry_solo_cc)
1134 def on_response_cb(self, dlg, response_id, *args):
1135 if response_id == Gtk.ResponseType.APPLY:
1136 name = self.entry_name.get_text()
1138 if name != self.channel.channel_name:
1139 self.channel.channel_name = name
1141 if self.channel and isinstance(self.channel, InputChannel):
1142 if self.direct_output.get_active() and not self.channel.post_fader_output_channel:
1143 self.channel.wants_direct_output = True
1144 self.app.add_direct_output(self.channel)
1145 elif (
1146 not self.direct_output.get_active() and self.channel.post_fader_output_channel
1148 self.channel.wants_direct_output = False
1149 self.channel.post_fader_output_channel.remove()
1150 self.channel.post_fader_output_channel = None
1152 for control in ("volume", "balance", "mute", "solo"):
1153 widget = getattr(self, "entry_{}_cc".format(control), None)
1154 if widget is not None:
1155 value = int(widget.get_value())
1156 if value != -1:
1157 setattr(self.channel.channel, "{}_midi_cc".format(control), value)
1159 self.hide()
1160 return True
1162 def on_entry_name_changed(self, entry):
1163 sensitive = False
1164 if len(entry.get_text()):
1165 if self.channel and self.channel.channel.name == entry.get_text():
1166 sensitive = True
1167 elif entry.get_text() not in [x.channel.name for x in self.app.channels] + [
1168 x.channel.name for x in self.app.output_channels
1170 sensitive = True
1171 self.ok_button.set_sensitive(sensitive)
1174 class NewChannelDialog(ChannelPropertiesDialog):
1175 def create_ui(self):
1176 super().create_ui()
1177 self.add_initial_vol_radio()
1178 self.vbox.show_all()
1180 def add_buttons(self):
1181 self.add_button(Gtk.STOCK_CANCEL, Gtk.ResponseType.CANCEL)
1182 self.ok_button = self.add_button(Gtk.STOCK_ADD, Gtk.ResponseType.OK)
1183 self.ok_button.set_sensitive(False)
1184 self.set_default_response(Gtk.ResponseType.OK)
1186 def add_initial_vol_radio(self):
1187 grid = self.properties_grid
1188 grid.attach(Gtk.Label(label=_("Value"), halign=Gtk.Align.START), 0, 2, 1, 1)
1189 self.minus_inf = Gtk.RadioButton.new_with_mnemonic(None, _("-_Inf"))
1190 self.zero_dB = Gtk.RadioButton.new_with_mnemonic_from_widget(self.minus_inf, _("_0dB"))
1191 grid.attach(self.minus_inf, 1, 2, 1, 1)
1192 grid.attach(self.zero_dB, 2, 2, 1, 1)
1195 class NewInputChannelDialog(NewChannelDialog):
1196 def __init__(self, app, title=None):
1197 super().__init__(app, title=title or _("New Input Channel"))
1199 def fill_ui(self, **values):
1200 self.entry_name.set_text(values.get("name", ""))
1201 self.direct_output.set_active(values.get("direct_output", True))
1202 # don't set MIDI CCs to previously used values, because they
1203 # would overwrite existing mappings, if accepted.
1204 self.entry_volume_cc.set_value(-1)
1205 self.entry_balance_cc.set_value(-1)
1206 self.entry_mute_cc.set_value(-1)
1207 self.entry_solo_cc.set_value(-1)
1208 self.stereo.set_active(values.get("stereo", True))
1209 self.minus_inf.set_active(values.get("initial_vol", -1) == -1)
1210 self.entry_name.grab_focus()
1212 def get_result(self):
1213 return {
1214 "name": self.entry_name.get_text(),
1215 "stereo": self.stereo.get_active(),
1216 "direct_output": self.direct_output.get_active(),
1217 "volume_cc": int(self.entry_volume_cc.get_value()),
1218 "balance_cc": int(self.entry_balance_cc.get_value()),
1219 "mute_cc": int(self.entry_mute_cc.get_value()),
1220 "solo_cc": int(self.entry_solo_cc.get_value()),
1221 "initial_vol": 0 if self.zero_dB.get_active() else -1,
1225 class OutputChannelPropertiesDialog(ChannelPropertiesDialog):
1226 def create_ui(self):
1227 super().create_ui()
1229 grid = self.properties_grid
1230 color_label = Gtk.Label.new_with_mnemonic(_("_Color"))
1231 color_label.set_halign(Gtk.Align.START)
1232 grid.attach(color_label, 0, 3, 1, 1)
1233 self.color_chooser_button = Gtk.ColorButton()
1234 self.color_chooser_button.set_use_alpha(True)
1235 color_label.set_mnemonic_widget(self.color_chooser_button)
1236 grid.attach(self.color_chooser_button, 1, 3, 2, 1)
1238 vbox = Gtk.Box(orientation=Gtk.Orientation.VERTICAL)
1239 self.vbox.pack_start(self.create_frame(_("Input Channels"), vbox), True, True, 0)
1241 self.display_solo_buttons = Gtk.CheckButton.new_with_mnemonic(_("_Display solo buttons"))
1242 vbox.pack_start(self.display_solo_buttons, True, True, 0)
1244 self.vbox.show_all()
1246 def fill_ui(self):
1247 super().fill_ui()
1248 self.display_solo_buttons.set_active(self.channel.display_solo_buttons)
1249 self.color_chooser_button.set_rgba(self.channel.color)
1251 def on_response_cb(self, dlg, response_id, *args):
1252 if response_id == Gtk.ResponseType.APPLY:
1253 self.channel.display_solo_buttons = self.display_solo_buttons.get_active()
1254 self.channel.set_color(self.color_chooser_button.get_rgba())
1255 for inputchannel in self.app.channels:
1256 inputchannel.update_control_group(self.channel)
1258 return super().on_response_cb(dlg, response_id, *args)
1261 class NewOutputChannelDialog(NewChannelDialog, OutputChannelPropertiesDialog):
1262 def __init__(self, app, title=None):
1263 super().__init__(app, title=title or _("New Output Channel"))
1265 def fill_ui(self, **values):
1266 self.entry_name.set_text(values.get("name", ""))
1268 # TODO: disable mode for output channels as mono output channels may
1269 # not be correctly handled yet.
1270 self.mono.set_sensitive(False)
1271 self.stereo.set_sensitive(False)
1273 # don't set MIDI CCs to previously used values, because they
1274 # would overwrite existing mappings, if accepted.
1275 self.entry_volume_cc.set_value(-1)
1276 self.entry_balance_cc.set_value(-1)
1277 self.entry_mute_cc.set_value(-1)
1278 self.stereo.set_active(values.get("stereo", True))
1279 self.minus_inf.set_active(values.get("initial_vol", -1) == -1)
1280 # choose a new random color for each new output channel
1281 self.color_chooser_button.set_rgba(random_color())
1282 self.display_solo_buttons.set_active(values.get("display_solo_buttons", False))
1283 self.entry_name.grab_focus()
1285 def get_result(self):
1286 return {
1287 "name": self.entry_name.get_text(),
1288 "stereo": self.stereo.get_active(),
1289 "volume_cc": int(self.entry_volume_cc.get_value()),
1290 "balance_cc": int(self.entry_balance_cc.get_value()),
1291 "mute_cc": int(self.entry_mute_cc.get_value()),
1292 "display_solo_buttons": self.display_solo_buttons.get_active(),
1293 "color": self.color_chooser_button.get_rgba(),
1294 "initial_vol": 0 if self.zero_dB.get_active() else -1,
1298 class ControlGroup(Gtk.Alignment):
1299 def __init__(self, output_channel, input_channel):
1300 super().__init__()
1301 self.set(0.5, 0.5, 1, 1)
1302 self.output_channel = output_channel
1303 self.input_channel = input_channel
1304 self.app = input_channel.app
1306 self.hbox = Gtk.Box(orientation=Gtk.Orientation.HORIZONTAL)
1307 self.vbox = Gtk.Box(orientation=Gtk.Orientation.VERTICAL)
1308 self.add(self.vbox)
1309 self.buttons_box = Gtk.Box(False, BUTTON_PADDING, orientation=Gtk.Orientation.HORIZONTAL)
1311 set_background_color(self.vbox, output_channel.css_name, output_channel.color)
1313 self.vbox.pack_start(self.hbox, True, True, BUTTON_PADDING)
1314 hbox_context = self.hbox.get_style_context()
1315 hbox_context.add_class("control_group")
1317 name = output_channel.channel.name
1318 self.label = Gtk.Label(name)
1319 self.label.get_style_context().add_class("label")
1320 self.label.set_max_width_chars(self.input_channel.label_chars_narrow)
1321 self.label.set_ellipsize(Pango.EllipsizeMode.MIDDLE)
1323 if len(name) > self.input_channel.label_chars_narrow:
1324 self.label.set_tooltip_text(name)
1326 self.hbox.pack_start(self.label, False, False, BUTTON_PADDING)
1327 self.hbox.pack_end(self.buttons_box, False, False, BUTTON_PADDING)
1329 self.mute = mute = Gtk.ToggleButton(_("M"))
1330 mute.get_style_context().add_class("mute")
1331 mute.set_tooltip_text(_("Mute output channel send"))
1332 mute.connect("button-press-event", self.on_mute_button_pressed)
1333 mute.connect("toggled", self.on_mute_toggled)
1335 self.solo = solo = Gtk.ToggleButton("S")
1336 solo.get_style_context().add_class("solo")
1337 solo.set_tooltip_text(_("Solo output send"))
1338 solo.connect("button-press-event", self.on_solo_button_pressed)
1339 solo.connect("toggled", self.on_solo_toggled)
1341 self.prefader = pre = Gtk.ToggleButton(_("P"))
1342 pre.get_style_context().add_class("prefader")
1343 pre.set_tooltip_text(_("Pre (on) / Post (off) fader send"))
1344 pre.connect("toggled", self.on_prefader_toggled)
1346 self.buttons_box.pack_start(pre, True, True, BUTTON_PADDING)
1347 self.buttons_box.pack_start(mute, True, True, BUTTON_PADDING)
1349 if self.output_channel.display_solo_buttons:
1350 self.buttons_box.pack_start(solo, True, True, BUTTON_PADDING)
1352 def update(self):
1353 if self.output_channel.display_solo_buttons:
1354 if self.solo not in self.buttons_box.get_children():
1355 self.buttons_box.pack_start(self.solo, True, True, BUTTON_PADDING)
1356 self.solo.show()
1357 else:
1358 if self.solo in self.buttons_box.get_children():
1359 self.buttons_box.remove(self.solo)
1361 name = self.output_channel.channel.name
1362 self.label.set_text(name)
1363 if len(name) > self.input_channel.label_chars_narrow:
1364 self.label.set_tooltip_text(name)
1366 set_background_color(self.vbox, self.output_channel.css_name, self.output_channel.color)
1368 def on_mute_toggled(self, button):
1369 self.output_channel.channel.set_muted(self.input_channel.channel, button.get_active())
1370 self.app.update_monitor(self.output_channel)
1372 def on_mute_button_pressed(self, button, event, *args):
1373 if event.button == 1 and event.state & Gdk.ModifierType.CONTROL_MASK:
1374 # Ctrlr+left-click => exclusive mute for output
1375 for channel in self.app.channels:
1376 if channel is not self.input_channel:
1377 ctlgroup = channel.get_control_group(self.output_channel)
1378 ctlgroup.mute.set_active(False)
1379 return button.get_active()
1381 def on_solo_toggled(self, button):
1382 self.output_channel.channel.set_solo(self.input_channel.channel, button.get_active())
1383 self.app.update_monitor(self.output_channel)
1385 def on_solo_button_pressed(self, button, event, *args):
1386 if event.button == 1 and event.state & Gdk.ModifierType.CONTROL_MASK:
1387 # Ctrlr+left-click => exclusive solo for output
1388 for channel in self.app.channels:
1389 if channel is not self.input_channel:
1390 ctlgroup = channel.get_control_group(self.output_channel)
1391 ctlgroup.solo.set_active(False)
1392 return button.get_active()
1394 def on_prefader_toggled(self, button):
1395 self.output_channel.channel.set_in_prefader(
1396 self.input_channel.channel, button.get_active()
1399 def narrow(self):
1400 self.hbox.remove(self.label)
1401 self.hbox.set_child_packing(self.buttons_box, True, True, BUTTON_PADDING, Gtk.PackType.END)
1403 def widen(self):
1404 self.hbox.pack_start(self.label, False, False, BUTTON_PADDING)
1405 self.hbox.set_child_packing(
1406 self.buttons_box, False, False, BUTTON_PADDING, Gtk.PackType.END
1410 GObject.signal_new(
1411 "input-channel-order-changed",
1412 InputChannel,
1413 GObject.SignalFlags.RUN_FIRST | GObject.SignalFlags.ACTION,
1414 None,
1415 [GObject.TYPE_STRING, GObject.TYPE_STRING],
1418 GObject.signal_new(
1419 "output-channel-order-changed",
1420 OutputChannel,
1421 GObject.SignalFlags.RUN_FIRST | GObject.SignalFlags.ACTION,
1422 None,
1423 [GObject.TYPE_STRING, GObject.TYPE_STRING],