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.
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
29 from .serialization
import SerializedObject
30 from .styling
import set_background_color
, random_color
33 log
= logging
.getLogger(__name__
)
37 class Channel(Gtk
.Box
, SerializedObject
):
38 """Widget with slider and meter used as base class for more specific
43 def __init__(self
, app
, name
, stereo
=True, direct_output
=True, initial_vol
=None):
44 super().__init
__(orientation
=Gtk
.Orientation
.VERTICAL
)
46 self
.mixer
= app
.mixer
48 self
.gui_factory
= app
.gui_factory
49 self
._channel
_name
= name
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
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 # ---------------------------------------------------------------------------------------------
76 def channel_name(self
):
77 return self
._channel
_name
80 def channel_name(self
, name
):
81 self
.app
.on_channel_rename(self
._channel
_name
, name
)
82 self
._channel
_name
= 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
)
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
):
98 parent
= self
.balance
.get_parent()
99 self
.balance
.destroy()
101 self
.balance
= slider
.BalanceSlider(self
.balance_adjustment
, (20, 20), (0, 100))
104 parent
.pack_end(self
.balance
, False, True, 0)
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
):
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
)
155 self
.slider
= slider
.VolumeSlider(self
.slider_adjustment
)
158 parent
.pack_start(self
.slider
, True, True, 0)
159 parent
.reorder_child(self
.slider
, 0)
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
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
)
185 self
.create_slider_widget()
188 self
.create_balance_widget()
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")
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")
207 self
.meter
= meter
.StereoMeterWidget(self
.meter_scale
)
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)
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)
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()
269 event
.state
& Gdk
.ModifierType
.CONTROL_MASK
270 and event
.type == Gdk
.EventType
.BUTTON_PRESS
271 and event
.button
== 1
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
)
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)
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()
323 log
.debug("Volume digits confirmation '%f dBFS'.", db
)
325 log
.debug("Volume digits confirmation ignore, reset to current.")
326 self
.update_volume(False)
328 self
.slider_adjustment
.set_value_db(db
)
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()
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()
359 elif event
.keyval
== Gdk
.KEY_Down
:
360 log
.debug(self
.channel_name
+ " Down")
361 self
.slider_adjustment
.step_down()
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
):
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
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
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):
400 self
.channel
.volume_midi_cc
= volume_cc
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
)
410 self
.channel
.balance_midi_cc
= balance_cc
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
)
420 self
.channel
.mute_midi_cc
= mute_cc
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:
431 self
.channel
.solo_midi_cc
= solo_cc
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 # ---------------------------------------------------------------------------------------------
442 def set_monitored(self
):
444 self
.app
.set_monitored_channel(self
)
445 self
.monitor_button
.set_active(True)
447 def set_color(self
, color
):
449 set_background_color(self
.label_name_event_box
, self
.css_name
, self
.color
)
451 def widen(self
, flag
=True):
453 ctx
= self
.label_name
.get_style_context()
456 ctx
.remove_class("narrow")
457 ctx
.add_class("wide")
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
477 def read_meter(self
):
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
)
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
)
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
)
504 db_text
= "%.2f" % db
505 self
.volume_digits
.set_text(db_text
)
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
):
534 self
.slider_adjustment
.set_value_db(float(value
))
536 elif name
== "balance":
537 self
.balance_adjustment
.set_value(float(value
))
539 elif name
== "out_mute":
540 self
.future_out_mute
= value
== "True"
542 elif name
== "volume_midi_cc":
543 self
.future_volume_midi_cc
= int(value
)
545 elif name
== "balance_midi_cc":
546 self
.future_balance_midi_cc
= int(value
)
548 elif name
== "mute_midi_cc":
549 self
.future_mute_midi_cc
= int(value
)
551 elif name
== "solo_midi_cc":
552 self
.future_solo_midi_cc
= int(value
)
555 self
.wide
= value
== "True"
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)
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"))
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)
601 self
.create_buttons()
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()
616 for cg
in self
.get_control_groups():
619 def widen(self
, flag
=True):
621 for cg
in self
.get_control_groups():
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
:
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)
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
:
652 def get_control_groups(self
):
654 for c
in self
.vbox
.get_children():
655 if isinstance(c
, ControlGroup
):
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
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
727 def serialization_name(cls
):
728 return "input_channel"
730 def serialize(self
, object_backend
):
731 object_backend
.add_property("name", self
.channel_name
)
733 object_backend
.add_property("type", "stereo")
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
):
741 self
.channel_name
= str(value
)
744 if value
== "stereo":
747 elif value
== "mono":
750 elif name
== "direct_output":
751 self
.wants_direct_output
= value
== "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()
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
)
783 def color(self
, value
):
784 if isinstance(value
, Gdk
.RGBA
):
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"))
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
)
814 self
.create_buttons()
816 # add control groups to the input channels, and initialize them
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
:
830 self
._init
_muted
_channels
= None
831 self
._init
_solo
_channels
= None
832 self
._init
_prefader
_channels
= None
838 # remove control groups from input channels
839 for input_channel
in self
.app
.channels
:
840 input_channel
.remove_control_group(self
)
843 self
.channel
.remove()
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
:
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()
866 def serialization_name(cls
):
867 return "output_channel"
869 def serialize(self
, object_backend
):
870 object_backend
.add_property("name", self
.channel_name
)
872 object_backend
.add_property("type", "stereo")
874 object_backend
.add_property("type", "mono")
875 if self
.display_solo_buttons
:
876 object_backend
.add_property("solo_buttons", "true")
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
)
888 object_backend
.add_property(
889 "muted_channels", "|".join([x
.channel
.name
for x
in muted_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
):
904 self
.channel_name
= str(value
)
907 if value
== "stereo":
910 elif value
== "mono":
913 elif name
== "solo_buttons":
915 self
.display_solo_buttons
= True
917 elif name
== "muted_channels":
918 self
._init
_muted
_channels
= value
.split("|")
920 elif name
== "solo_channels":
921 self
._init
_solo
_channels
= value
.split("|")
923 elif name
== "prefader_channels":
924 self
._init
_prefader
_channels
= value
.split("|")
926 elif name
== "color":
932 return super().unserialize_property(name
, value
)
935 class ChannelPropertiesDialog(Gtk
.Dialog
):
936 def __init__(self
, app
, channel
=None, title
=None):
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
945 self
.mixer
= app
.mixer
946 self
.set_default_size(365, -1)
950 def fill_and_show(self
):
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
)
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)
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)
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()
1071 self
.entry_name
.set_text(self
.channel
.channel_name
)
1072 if self
.channel
.channel
.is_stereo
:
1073 self
.stereo
.set_active(True)
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
)
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
):
1106 timeout_label
.set_text(
1107 _("This window will close in {seconds} seconds.").format(seconds
=window
.timeout
)
1109 if window
.timeout
== 0:
1111 entry
.set_value(self
.mixer
.last_midi_cc
)
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
)
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())
1157 setattr(self
.channel
.channel
, "{}_midi_cc".format(control
), value
)
1162 def on_entry_name_changed(self
, entry
):
1164 if len(entry
.get_text()):
1165 if self
.channel
and self
.channel
.channel
.name
== entry
.get_text():
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
1171 self
.ok_button
.set_sensitive(sensitive
)
1174 class NewChannelDialog(ChannelPropertiesDialog
):
1175 def create_ui(self
):
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
):
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
):
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()
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
):
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
):
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
)
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
)
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
)
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()
1400 self
.hbox
.remove(self
.label
)
1401 self
.hbox
.set_child_packing(self
.buttons_box
, True, True, BUTTON_PADDING
, Gtk
.PackType
.END
)
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
1411 "input-channel-order-changed",
1413 GObject
.SignalFlags
.RUN_FIRST | GObject
.SignalFlags
.ACTION
,
1415 [GObject
.TYPE_STRING
, GObject
.TYPE_STRING
],
1419 "output-channel-order-changed",
1421 GObject
.SignalFlags
.RUN_FIRST | GObject
.SignalFlags
.ACTION
,
1423 [GObject
.TYPE_STRING
, GObject
.TYPE_STRING
],