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.
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
32 log = logging.getLogger(__name__)
53 border: 1px inset #111;
60 border: 1px inset #111;
64 css_provider = Gtk.CssProvider()
65 css_provider.load_from_data(CSS)
66 context = Gtk.StyleContext()
67 screen = Gdk.Screen.get_default()
68 context.add_provider_for_screen(screen, css_provider, Gtk.STYLE_PROVIDER_PRIORITY_APPLICATION)
71 def get_text_color(background_color):
72 """Calculates the luminance of the given color (GdkRGBA)
73 and returns an appropriate text color."""
74 # luminance coefficients taken from section C-9 from
75 # http://www.faqs.org/faqs/graphics/colorspace-faq/
76 brightess = background_color.red * 0.212671 + \
77 background_color.green * 0.715160 + \
78 background_color.blue * 0.072169
86 def set_background_color(widget, name, color):
87 color_string = color.to_string()
93 """ % (name, color_string, get_text_color(color))
95 css_provider = Gtk.CssProvider()
96 css_provider.load_from_data(css.encode('utf-8'))
97 context = Gtk.StyleContext()
98 screen = Gdk.Screen.get_default()
99 context.add_provider_for_screen(screen, css_provider, Gtk.STYLE_PROVIDER_PRIORITY_APPLICATION)
101 widget_context = widget.get_style_context()
102 widget_context.add_class(name)
106 from random import uniform, seed
108 return Gdk.RGBA(uniform(0, 1), uniform(0, 1), uniform(0, 1), 1)
111 class Channel(Gtk.VBox, SerializedObject):
112 """Widget with slider and meter used as base class for more specific
115 monitor_button = None
118 def __init__(self, app, name, stereo, value = None):
119 Gtk.VBox.__init__(self)
121 self.mixer = app.mixer
123 self.gui_factory = app.gui_factory
124 self._channel_name = name
126 self.initial_value = value
127 self.meter_scale = self.gui_factory.get_default_meter_scale()
128 self.slider_scale = self.gui_factory.get_default_slider_scale()
129 self.slider_adjustment = slider.AdjustmentdBFS(self.slider_scale, 0.0, 0.02)
130 self.balance_adjustment = slider.BalanceAdjustment()
131 self.post_fader_output_channel = None
132 self.future_out_mute = None
133 self.future_volume_midi_cc = None
134 self.future_balance_midi_cc = None
135 self.future_mute_midi_cc = None
136 self.future_solo_midi_cc = None
137 self.css_name = "css_name_%d" % Channel.num_instances
138 self.label_name = None
140 self.label_chars_wide = 12
141 self.label_chars_narrow = 7
142 Channel.num_instances += 1
144 def get_channel_name(self):
145 return self._channel_name
147 def set_channel_name(self, name):
148 self.app.on_channel_rename(self._channel_name, name);
149 self._channel_name = name
151 self.label_name.set_text(name)
152 if len(name) > (self.label_chars_wide if self.wide else self.label_chars_narrow):
153 self.label_name.set_tooltip_text(name)
155 self.channel.name = name
156 if self.post_fader_output_channel:
157 self.post_fader_output_channel.name = "%s Out" % name;
158 channel_name = property(get_channel_name, set_channel_name)
160 def create_balance_widget(self):
161 self.balance = slider.BalanceSlider(self.balance_adjustment, (20, 20), (0, 100))
164 def create_buttons(self):
165 # Mute, Solo and Monitor buttons
166 self.hbox_mutesolo = Gtk.Box(False, 0, orientation=Gtk.Orientation.HORIZONTAL)
168 self.mute = Gtk.ToggleButton()
169 self.mute.set_label("M")
170 self.mute.set_name("mute")
171 self.mute.set_active(self.channel.out_mute)
172 self.mute.connect("toggled", self.on_mute_toggled)
173 self.hbox_mutesolo.pack_start(self.mute, True, True, 0)
175 self.pack_start(self.hbox_mutesolo, False, False, 0)
177 self.monitor_button = Gtk.ToggleButton('MON')
178 self.monitor_button.connect('toggled', self.on_monitor_button_toggled)
179 self.pack_start(self.monitor_button, False, False, 0)
181 def create_fader(self):
182 # HBox for fader and meter
183 self.vbox_fader = Gtk.Box(orientation=Gtk.Orientation.VERTICAL)
184 self.vbox_fader.get_style_context().add_class('vbox_fader')
186 self.hbox_readouts = Gtk.Box(orientation=Gtk.Orientation.HORIZONTAL)
187 self.hbox_readouts.set_homogeneous(True)
188 self.hbox_readouts.pack_start(self.volume_digits, False, True, 0)
189 self.hbox_readouts.pack_start(self.abspeak, False, True, 0)
190 self.vbox_fader.pack_start(self.hbox_readouts, False, False, 0)
192 self.hbox_fader = Gtk.Box(orientation=Gtk.Orientation.HORIZONTAL)
193 self.hbox_fader.pack_start(self.slider, True, True, 0)
194 self.hbox_fader.pack_start(self.meter, True, True, 0)
195 self.vbox_fader.pack_start(self.hbox_fader, True, True, 0)
196 self.vbox_fader.pack_start(self.balance, False, True, 0)
198 self.pack_start(self.vbox_fader, True, True, 0)
200 def create_slider_widget(self):
203 parent = self.slider.get_parent()
204 self.slider.destroy()
206 if self.gui_factory.use_custom_widgets:
207 self.slider = slider.CustomSliderWidget(self.slider_adjustment)
209 self.slider = slider.VolumeSlider(self.slider_adjustment)
212 parent.pack_start(self.slider, True, True, 0)
213 parent.reorder_child(self.slider, 0)
218 log.debug('Realizing channel "%s".', self.channel_name)
219 if self.future_out_mute != None:
220 self.channel.out_mute = self.future_out_mute
223 # Channel strip label
224 self.vbox = Gtk.Box(orientation=Gtk.Orientation.VERTICAL)
225 self.pack_start(self.vbox, False, True, 0)
226 self.label_name = Gtk.Label()
227 self.label_name.get_style_context().add_class('top_label')
228 self.label_name.set_text(self.channel_name)
229 self.label_name.set_max_width_chars(self.label_chars_wide if self.wide else
230 self.label_chars_narrow)
231 self.label_name.set_ellipsize(Pango.EllipsizeMode.MIDDLE)
232 self.label_name_event_box = Gtk.EventBox()
233 self.label_name_event_box.connect('button-press-event', self.on_label_mouse)
234 self.label_name_event_box.add(self.label_name)
238 self.create_slider_widget()
239 self.create_balance_widget()
242 self.volume_digits = Gtk.Entry()
243 self.volume_digits.set_has_frame(False)
244 self.volume_digits.set_width_chars(5)
245 self.volume_digits.set_property('xalign', 0.5)
246 self.volume_digits.connect("key-press-event", self.on_volume_digits_key_pressed)
247 self.volume_digits.connect("focus-out-event", self.on_volume_digits_focus_out)
248 self.volume_digits.get_style_context().add_class('readout')
251 self.abspeak = abspeak.AbspeakWidget()
252 self.abspeak.connect("reset", self.on_abspeak_reset)
253 self.abspeak.connect("volume-adjust", self.on_abspeak_adjust)
254 self.abspeak.get_style_context().add_class('readout')
258 self.meter = meter.StereoMeterWidget(self.meter_scale)
260 self.meter = meter.MonoMeterWidget(self.meter_scale)
262 self.meter.set_events(Gdk.EventMask.SCROLL_MASK)
263 self.on_vumeter_color_changed(self.gui_factory)
265 if self.initial_value != None:
266 if self.initial_value == True:
267 self.slider_adjustment.set_value(0)
269 self.slider_adjustment.set_value_db(0)
271 self.slider_adjustment.connect("volume-changed", self.on_volume_changed)
272 self.slider_adjustment.connect("volume-changed-from-midi", self.on_volume_changed_from_midi)
273 self.balance_adjustment.connect("balance-changed", self.on_balance_changed)
275 self.gui_factory.connect("default-meter-scale-changed", self.on_default_meter_scale_changed)
276 self.gui_factory.connect("default-slider-scale-changed", self.on_default_slider_scale_changed)
277 self.gui_factory.connect('vumeter-color-changed', self.on_vumeter_color_changed)
278 self.gui_factory.connect('vumeter-color-scheme-changed', self.on_vumeter_color_changed)
279 self.gui_factory.connect('use-custom-widgets-changed', self.on_custom_widgets_changed)
281 self.connect("key-press-event", self.on_key_pressed)
282 self.connect("scroll-event", self.on_scroll)
285 log.debug('Unrealizing channel "%s".', self.channel_name)
287 def widen(self, flag=True):
289 ctx = self.label_name.get_style_context()
292 ctx.remove_class('narrow')
293 ctx.add_class('wide')
295 ctx.remove_class('wide')
296 ctx.add_class('narrow')
298 label = self.label_name.get_label()
299 label_width = self.label_chars_wide if flag else self.label_chars_narrow
300 self.label_name.set_max_width_chars(label_width)
302 if len(label) > label_width:
303 self.label_name.set_tooltip_text(label)
305 self.meter.widen(flag)
306 self.hbox_readouts.set_orientation(
307 Gtk.Orientation.HORIZONTAL if flag else Gtk.Orientation.VERTICAL)
312 def on_label_mouse(self, widget, event):
313 if event.type == Gdk.EventType._2BUTTON_PRESS:
314 if event.button == 1:
315 self.on_channel_properties()
317 elif (event.state & Gdk.ModifierType.CONTROL_MASK and
318 event.type == Gdk.EventType.BUTTON_PRESS and
326 def on_default_meter_scale_changed(self, gui_factory, scale):
327 log.debug("Default meter scale change detected.")
328 self.meter.set_scale(scale)
330 def on_default_slider_scale_changed(self, gui_factory, scale):
331 log.debug("Default slider scale change detected.")
332 self.slider_scale = scale
333 self.slider_adjustment.set_scale(scale)
335 self.channel.midi_scale = self.slider_scale.scale
337 def on_vumeter_color_changed(self, gui_factory, *args):
338 color = gui_factory.get_vumeter_color()
339 color_scheme = gui_factory.get_vumeter_color_scheme()
340 if color_scheme != 'solid':
341 self.meter.set_color(None)
343 self.meter.set_color(Gdk.color_parse(color))
345 def on_custom_widgets_changed(self, gui_factory, value):
346 self.balance.destroy()
347 self.create_balance_widget()
348 self.create_slider_widget()
350 def on_abspeak_adjust(self, abspeak, adjust):
351 log.debug("abspeak adjust %f", adjust)
352 self.slider_adjustment.set_value_db(self.slider_adjustment.get_value_db() + adjust)
353 self.channel.abspeak = None
354 #self.update_volume(False) # We want to update gui even if actual decibels have not changed (scale wrap for example)
356 def on_abspeak_reset(self, abspeak):
357 log.debug("abspeak reset")
358 self.channel.abspeak = None
360 def on_volume_digits_key_pressed(self, widget, event):
361 if (event.keyval == Gdk.KEY_Return or event.keyval == Gdk.KEY_KP_Enter):
362 db_text = self.volume_digits.get_text()
365 log.debug('Volume digits confirmation "%f dBFS".', db)
366 except (ValueError) as e:
367 log.debug("Volume digits confirmation ignore, reset to current.")
368 self.update_volume(False)
370 self.slider_adjustment.set_value_db(db)
372 #self.update_volume(False) # We want to update gui even if actual decibels have not changed (scale wrap for example)
374 def on_volume_digits_focus_out(self, widget, event):
375 log.debug("Volume digits focus out detected.")
376 self.update_volume(False)
378 def read_meter(self):
382 peak_left, peak_right, rms_left, rms_right = self.channel.kmeter
383 self.meter.set_values(peak_left, peak_right, rms_left, rms_right)
385 peak, rms = self.channel.kmeter
386 self.meter.set_values(peak, rms)
388 self.abspeak.set_peak(self.channel.abspeak)
390 def on_scroll(self, widget, event):
391 if event.direction == Gdk.ScrollDirection.DOWN:
392 self.slider_adjustment.step_down()
393 elif event.direction == Gdk.ScrollDirection.UP:
394 self.slider_adjustment.step_up()
397 def update_volume(self, update_engine, from_midi = False):
398 db = self.slider_adjustment.get_value_db()
400 db_text = "%.2f" % db
401 self.volume_digits.set_text(db_text)
405 self.channel.volume = db
406 self.app.update_monitor(self)
408 def on_volume_changed(self, adjustment):
409 self.update_volume(True)
411 def on_volume_changed_from_midi(self, adjustment):
412 self.update_volume(True, from_midi = True)
414 def on_balance_changed(self, adjustment):
415 balance = self.balance_adjustment.get_value()
416 log.debug("%s balance: %f", self.channel_name, balance)
417 self.channel.balance = balance
418 self.app.update_monitor(self)
420 def on_key_pressed(self, widget, event):
421 if (event.keyval == Gdk.KEY_Up):
422 log.debug(self.channel_name + " Up")
423 self.slider_adjustment.step_up()
425 elif (event.keyval == Gdk.KEY_Down):
426 log.debug(self.channel_name + " Down")
427 self.slider_adjustment.step_down()
432 def serialize(self, object_backend):
433 object_backend.add_property("volume", "%f" % self.slider_adjustment.get_value_db())
434 object_backend.add_property("balance", "%f" % self.balance_adjustment.get_value())
435 object_backend.add_property("wide", "%s" % str(self.wide))
437 if hasattr(self.channel, 'out_mute'):
438 object_backend.add_property('out_mute', str(self.channel.out_mute))
439 if self.channel.volume_midi_cc != -1:
440 object_backend.add_property('volume_midi_cc', str(self.channel.volume_midi_cc))
441 if self.channel.balance_midi_cc != -1:
442 object_backend.add_property('balance_midi_cc', str(self.channel.balance_midi_cc))
443 if self.channel.mute_midi_cc != -1:
444 object_backend.add_property('mute_midi_cc', str(self.channel.mute_midi_cc))
445 if self.channel.solo_midi_cc != -1:
446 object_backend.add_property('solo_midi_cc', str(self.channel.solo_midi_cc))
448 def unserialize_property(self, name, value):
450 self.slider_adjustment.set_value_db(float(value))
452 if name == "balance":
453 self.balance_adjustment.set_value(float(value))
455 if name == 'out_mute':
456 self.future_out_mute = (value == 'True')
458 if name == 'volume_midi_cc':
459 self.future_volume_midi_cc = int(value)
461 if name == 'balance_midi_cc':
462 self.future_balance_midi_cc = int(value)
464 if name == 'mute_midi_cc':
465 self.future_mute_midi_cc = int(value)
467 if name == 'solo_midi_cc':
468 self.future_solo_midi_cc = int(value)
471 self.wide = value == "True"
475 def on_midi_event_received(self, *args):
476 self.slider_adjustment.set_value_db(self.channel.volume, from_midi = True)
477 self.balance_adjustment.set_balance(self.channel.balance, from_midi = True)
479 def on_monitor_button_toggled(self, button):
480 if button.get_active():
481 for channel in self.app.channels + self.app.output_channels:
482 if channel.monitor_button.get_active() and channel.monitor_button is not button:
483 channel.monitor_button.handler_block_by_func(
484 channel.on_monitor_button_toggled)
485 channel.monitor_button.set_active(False)
486 channel.monitor_button.handler_unblock_by_func(
487 channel.on_monitor_button_toggled)
488 self.app.set_monitored_channel(self)
490 if self.app._monitored_channel.channel.name == self.channel.name:
491 self.monitor_button.handler_block_by_func(self.on_monitor_button_toggled)
492 self.monitor_button.set_active(True)
493 self.monitor_button.handler_unblock_by_func(self.on_monitor_button_toggled)
495 def set_monitored(self):
497 self.app.set_monitored_channel(self)
498 self.monitor_button.set_active(True)
500 def set_color(self, color):
502 set_background_color(self.label_name_event_box, self.css_name, self.color)
505 class InputChannel(Channel):
506 post_fader_output_channel = None
508 def create_buttons(self):
509 super().create_buttons()
510 self.solo = Gtk.ToggleButton()
511 self.solo.set_label("S")
512 self.solo.set_name("solo")
513 self.solo.set_active(self.channel.solo)
514 self.solo.connect("toggled", self.on_solo_toggled)
515 self.hbox_mutesolo.pack_start(self.solo, True, True, 0)
518 self.channel = self.mixer.add_channel(self.channel_name, self.stereo)
520 if self.channel == None:
521 raise Exception("Cannot create a channel")
525 if self.future_volume_midi_cc != None:
526 self.channel.volume_midi_cc = self.future_volume_midi_cc
527 if self.future_balance_midi_cc != None:
528 self.channel.balance_midi_cc = self.future_balance_midi_cc
529 if self.future_mute_midi_cc != None:
530 self.channel.mute_midi_cc = self.future_mute_midi_cc
531 if self.future_solo_midi_cc != None:
532 self.channel.solo_midi_cc = self.future_solo_midi_cc
533 if self.app._init_solo_channels and self.channel_name in self.app._init_solo_channels:
534 self.channel.solo = True
536 self.channel.midi_scale = self.slider_scale.scale
538 self.on_volume_changed(self.slider_adjustment)
539 self.on_balance_changed(self.balance_adjustment)
541 entries = [Gtk.TargetEntry.new("INPUT_CHANNEL", Gtk.TargetFlags.SAME_APP, 0)]
542 self.label_name_event_box.drag_source_set(Gdk.ModifierType.BUTTON1_MASK, entries,
544 self.label_name_event_box.connect("drag-data-get", self.on_drag_data_get)
545 self.drag_dest_set(Gtk.DestDefaults.ALL, entries, Gdk.DragAction.MOVE)
546 self.connect_after("drag-data-received", self.on_drag_data_received)
548 self.vbox.pack_start(self.label_name_event_box, True, True, 0)
551 self.create_buttons()
558 if self.post_fader_output_channel:
559 self.post_fader_output_channel.remove()
560 self.post_fader_output_channel = None
561 self.channel.remove()
566 for cg in self.get_control_groups():
569 def widen(self, flag=True):
571 for cg in self.get_control_groups():
574 def on_drag_data_get(self, widget, drag_context, data, info, time):
575 channel = widget.get_parent().get_parent()
576 data.set(data.get_target(), 8, channel._channel_name.encode('utf-8'))
578 def on_drag_data_received(self, widget, drag_context, x, y, data, info, time):
579 source_name = data.get_data().decode('utf-8')
580 if source_name == self._channel_name:
582 self.emit("input-channel-order-changed", source_name, self._channel_name)
584 def add_control_group(self, channel):
585 control_group = ControlGroup(channel, self)
586 control_group.show_all()
587 self.vbox.pack_start(control_group, True, True, 0)
590 def remove_control_group(self, channel):
591 ctlgroup = self.get_control_group(channel)
592 self.vbox.remove(ctlgroup)
594 def update_control_group(self, channel):
595 for control_group in self.vbox.get_children():
596 if isinstance(control_group, ControlGroup):
597 if control_group.output_channel is channel:
598 control_group.update()
600 def get_control_group(self, channel):
601 for control_group in self.get_control_groups():
602 if control_group.output_channel is channel:
606 def get_control_groups(self):
608 for c in self.vbox.get_children():
609 if isinstance(c, ControlGroup):
613 channel_properties_dialog = None
615 def on_channel_properties(self):
616 if not self.channel_properties_dialog:
617 self.channel_properties_dialog = ChannelPropertiesDialog(self, self.app)
618 self.channel_properties_dialog.show()
619 self.channel_properties_dialog.present()
621 def on_mute_toggled(self, button):
622 self.channel.out_mute = self.mute.get_active()
624 def on_solo_toggled(self, button):
625 self.channel.solo = self.solo.get_active()
627 def midi_events_check(self):
628 if hasattr(self, 'channel') and self.channel.midi_in_got_events:
629 self.mute.set_active(self.channel.out_mute)
630 self.solo.set_active(self.channel.solo)
631 super().on_midi_event_received()
633 def on_solo_button_pressed(self, button, event, *args):
634 if event.button == 3:
635 # right click on the solo button, act on all output channels
636 if button.get_active(): # was soloed
637 button.set_active(False)
638 if hasattr(button, 'touched_channels'):
639 touched_channels = button.touched_channels
640 for chan in touched_channels:
641 ctlgroup = self.get_control_group(chan)
642 ctlgroup.solo.set_active(False)
643 del button.touched_channels
644 else: # was not soloed
645 button.set_active(True)
646 touched_channels = []
647 for chan in self.app.output_channels:
648 ctlgroup = self.get_control_group(chan)
649 if not ctlgroup.solo.get_active():
650 ctlgroup.solo.set_active(True)
651 touched_channels.append(chan)
652 button.touched_channels = touched_channels
657 def serialization_name(cls):
658 return 'input_channel'
660 def serialize(self, object_backend):
661 object_backend.add_property("name", self.channel_name)
663 object_backend.add_property("type", "stereo")
665 object_backend.add_property("type", "mono")
666 super().serialize(object_backend)
668 def unserialize_property(self, name, value):
670 self.channel_name = str(value)
673 if value == "stereo":
679 return super().unserialize_property(name, value)
682 GObject.signal_new("input-channel-order-changed", InputChannel,
683 GObject.SignalFlags.RUN_FIRST | GObject.SignalFlags.ACTION,
684 None, [GObject.TYPE_STRING, GObject.TYPE_STRING])
687 class OutputChannel(Channel):
688 _display_solo_buttons = False
690 _init_muted_channels = None
691 _init_solo_channels = None
692 _init_prefader_channels = None
694 channel_properties_dialog = None
696 def get_display_solo_buttons(self):
697 return self._display_solo_buttons
699 def set_display_solo_buttons(self, value):
700 self._display_solo_buttons = value
701 # notifying control groups
702 for inputchannel in self.app.channels:
703 inputchannel.update_control_group(self)
705 display_solo_buttons = property(get_display_solo_buttons, set_display_solo_buttons)
708 self.channel = self.mixer.add_output_channel(self.channel_name, self.stereo)
710 if self.channel == None:
711 raise Exception("Cannot create a channel")
715 if self.future_volume_midi_cc != None:
716 self.channel.volume_midi_cc = self.future_volume_midi_cc
717 if self.future_balance_midi_cc != None:
718 self.channel.balance_midi_cc = self.future_balance_midi_cc
719 if self.future_mute_midi_cc != None:
720 self.channel.mute_midi_cc = self.future_mute_midi_cc
721 self.channel.midi_scale = self.slider_scale.scale
723 self.on_volume_changed(self.slider_adjustment)
724 self.on_balance_changed(self.balance_adjustment)
726 entries = [Gtk.TargetEntry.new("OUTPUT_CHANNEL", Gtk.TargetFlags.SAME_APP, 0)]
727 self.label_name_event_box.drag_source_set(Gdk.ModifierType.BUTTON1_MASK, entries,
729 self.label_name_event_box.connect("drag-data-get", self.on_drag_data_get)
730 self.drag_dest_set(Gtk.DestDefaults.ALL, entries, Gdk.DragAction.MOVE)
731 self.connect_after("drag-data-received", self.on_drag_data_received)
733 if not hasattr(self, 'color'):
734 self.color = random_color()
735 set_background_color(self.label_name_event_box, self.css_name, self.color)
736 self.vbox.pack_start(self.label_name_event_box, True, True, 0)
739 self.create_buttons()
741 # add control groups to the input channels, and initialize them
743 for input_channel in self.app.channels:
744 ctlgroup = input_channel.add_control_group(self)
745 if self._init_muted_channels and input_channel.channel.name in self._init_muted_channels:
746 ctlgroup.mute.set_active(True)
747 if self._init_solo_channels and input_channel.channel.name in self._init_solo_channels:
748 ctlgroup.solo.set_active(True)
749 if self._init_prefader_channels and input_channel.channel.name in self._init_prefader_channels:
750 ctlgroup.prefader.set_active(True)
751 if not input_channel.wide:
754 self._init_muted_channels = None
755 self._init_solo_channels = None
756 self._init_prefader_channels = None
762 # remove control groups from input channels
763 for input_channel in self.app.channels:
764 input_channel.remove_control_group(self)
767 self.channel.remove()
770 def on_drag_data_get(self, widget, drag_context, data, info, time):
771 channel = widget.get_parent().get_parent()
772 data.set(data.get_target(), 8, channel._channel_name.encode('utf-8'))
774 def on_drag_data_received(self, widget, drag_context, x, y, data, info, time):
775 source_name = data.get_data().decode('utf-8')
776 if source_name == self._channel_name:
778 self.emit("output-channel-order-changed", source_name, self._channel_name)
780 def on_channel_properties(self):
781 if not self.channel_properties_dialog:
782 self.channel_properties_dialog = OutputChannelPropertiesDialog(self, self.app)
783 self.channel_properties_dialog.show()
784 self.channel_properties_dialog.present()
786 def on_mute_toggled(self, button):
787 self.channel.out_mute = self.mute.get_active()
789 def midi_events_check(self):
790 if self.channel != None and self.channel.midi_in_got_events:
791 self.mute.set_active(self.channel.out_mute)
792 super().on_midi_event_received()
795 def serialization_name(cls):
796 return 'output_channel'
798 def serialize(self, object_backend):
799 object_backend.add_property("name", self.channel_name)
801 object_backend.add_property("type", "stereo")
803 object_backend.add_property("type", "mono")
804 if self.display_solo_buttons:
805 object_backend.add_property("solo_buttons", "true")
808 prefader_in_channels = []
809 for input_channel in self.app.channels:
810 if self.channel.is_muted(input_channel.channel):
811 muted_channels.append(input_channel)
812 if self.channel.is_solo(input_channel.channel):
813 solo_channels.append(input_channel)
814 if self.channel.is_in_prefader(input_channel.channel):
815 prefader_in_channels.append(input_channel)
817 object_backend.add_property('muted_channels', '|'.join([x.channel.name for x in muted_channels]))
819 object_backend.add_property('solo_channels', '|'.join([x.channel.name for x in solo_channels]))
820 if prefader_in_channels:
821 object_backend.add_property('prefader_channels', '|'.join([x.channel.name for x in prefader_in_channels]))
822 object_backend.add_property("color", self.color.to_string())
823 super().serialize(object_backend)
825 def unserialize_property(self, name, value):
827 self.channel_name = str(value)
830 if value == "stereo":
836 if name == "solo_buttons":
838 self.display_solo_buttons = True
840 if name == 'muted_channels':
841 self._init_muted_channels = value.split('|')
843 if name == 'solo_channels':
844 self._init_solo_channels = value.split('|')
846 if name == 'prefader_channels':
847 self._init_prefader_channels = value.split('|')
854 return super().unserialize_property(name, value)
857 class ChannelPropertiesDialog(Gtk.Dialog):
860 def __init__(self, parent, app):
861 self.channel = parent
863 self.mixer = self.channel.mixer
864 Gtk.Dialog.__init__(self, 'Channel "%s" Properties' % self.channel.channel_name, app.window)
865 self.set_default_size(365, -1)
867 self.add_button(Gtk.STOCK_CANCEL, Gtk.ResponseType.CANCEL)
868 self.ok_button = self.add_button(Gtk.STOCK_APPLY, Gtk.ResponseType.APPLY)
869 self.set_default_response(Gtk.ResponseType.APPLY);
874 self.connect('response', self.on_response_cb)
875 self.connect('delete-event', self.on_response_cb)
877 def create_frame(self, label, child, padding=8):
878 # need to pass an empty label, otherwise no label widget is created
879 frame = Gtk.Frame(label='')
880 frame.get_label_widget().set_markup('<b>%s</b>' % label)
881 frame.set_border_width(3)
882 frame.set_shadow_type(Gtk.ShadowType.NONE)
884 alignment = Gtk.Alignment.new(0.5, 0, 1, 1)
885 alignment.set_padding(padding, padding, padding, padding)
892 vbox = self.get_content_area()
894 self.properties_grid = grid = Gtk.Grid()
895 vbox.pack_start(self.create_frame('Properties', grid), True, True, 0)
896 grid.set_row_spacing(8)
897 grid.set_column_spacing(8)
898 grid.set_column_homogeneous(True)
900 name_label = Gtk.Label.new_with_mnemonic('_Name')
901 name_label.set_halign(Gtk.Align.START)
902 grid.attach(name_label, 0, 0, 1, 1)
903 self.entry_name = Gtk.Entry()
904 self.entry_name.set_activates_default(True)
905 self.entry_name.connect('changed', self.on_entry_name_changed)
906 name_label.set_mnemonic_widget(self.entry_name)
907 grid.attach(self.entry_name, 1, 0, 2, 1)
909 grid.attach(Gtk.Label(label='Mode', halign=Gtk.Align.START), 0, 1, 1, 1)
910 self.mono = Gtk.RadioButton.new_with_mnemonic(None, '_Mono')
911 self.stereo = Gtk.RadioButton.new_with_mnemonic_from_widget(self.mono, '_Stereo')
912 grid.attach(self.mono, 1, 1, 1, 1)
913 grid.attach(self.stereo, 2, 1, 1, 1)
916 vbox.pack_start(self.create_frame('MIDI Control Changes', grid), True, True, 0)
917 grid.set_row_spacing(8)
918 grid.set_column_spacing(8)
919 grid.set_column_homogeneous(True)
921 cc_tooltip = "{} MIDI Control Change number (0-127, set to -1 to assign next free CC #)"
922 volume_label = Gtk.Label.new_with_mnemonic('_Volume')
923 volume_label.set_halign(Gtk.Align.START)
924 grid.attach(volume_label, 0, 0, 1, 1)
925 self.entry_volume_cc = Gtk.SpinButton.new_with_range(-1, 127, 1)
926 self.entry_volume_cc.set_tooltip_text(cc_tooltip.format("Volume"))
927 volume_label.set_mnemonic_widget(self.entry_volume_cc)
928 grid.attach(self.entry_volume_cc, 1, 0, 1, 1)
929 self.button_sense_midi_volume = Gtk.Button('Learn')
930 self.button_sense_midi_volume.connect('clicked',
931 self.on_sense_midi_volume_clicked)
932 grid.attach(self.button_sense_midi_volume, 2, 0, 1, 1)
934 balance_label = Gtk.Label.new_with_mnemonic('_Balance')
935 balance_label.set_halign(Gtk.Align.START)
936 grid.attach(balance_label, 0, 1, 1, 1)
937 self.entry_balance_cc = Gtk.SpinButton.new_with_range(-1, 127, 1)
938 self.entry_balance_cc.set_tooltip_text(cc_tooltip.format("Balance"))
939 balance_label.set_mnemonic_widget(self.entry_balance_cc)
940 grid.attach(self.entry_balance_cc, 1, 1, 1, 1)
941 self.button_sense_midi_balance = Gtk.Button('Learn')
942 self.button_sense_midi_balance.connect('clicked',
943 self.on_sense_midi_balance_clicked)
944 grid.attach(self.button_sense_midi_balance, 2, 1, 1, 1)
946 mute_label = Gtk.Label.new_with_mnemonic('M_ute')
947 mute_label.set_halign(Gtk.Align.START)
948 grid.attach(mute_label, 0, 2, 1, 1)
949 self.entry_mute_cc = Gtk.SpinButton.new_with_range(-1, 127, 1)
950 self.entry_mute_cc.set_tooltip_text(cc_tooltip.format("Mute"))
951 mute_label.set_mnemonic_widget(self.entry_mute_cc)
952 grid.attach(self.entry_mute_cc, 1, 2, 1, 1)
953 self.button_sense_midi_mute = Gtk.Button('Learn')
954 self.button_sense_midi_mute.connect('clicked',
955 self.on_sense_midi_mute_clicked)
956 grid.attach(self.button_sense_midi_mute, 2, 2, 1, 1)
958 if (isinstance(self, NewChannelDialog) or (self.channel and
959 isinstance(self.channel, InputChannel))):
960 solo_label = Gtk.Label.new_with_mnemonic('S_olo')
961 solo_label.set_halign(Gtk.Align.START)
962 grid.attach(solo_label, 0, 3, 1, 1)
963 self.entry_solo_cc = Gtk.SpinButton.new_with_range(-1, 127, 1)
964 self.entry_solo_cc.set_tooltip_text(cc_tooltip.format("Solo"))
965 solo_label.set_mnemonic_widget(self.entry_solo_cc)
966 grid.attach(self.entry_solo_cc, 1, 3, 1, 1)
967 self.button_sense_midi_solo = Gtk.Button('Learn')
968 self.button_sense_midi_solo.connect('clicked',
969 self.on_sense_midi_solo_clicked)
970 grid.attach(self.button_sense_midi_solo, 2, 3, 1, 1)
975 self.entry_name.set_text(self.channel.channel_name)
976 if self.channel.channel.is_stereo:
977 self.stereo.set_active(True)
979 self.mono.set_active(True)
980 self.mono.set_sensitive(False)
981 self.stereo.set_sensitive(False)
982 self.entry_volume_cc.set_value(self.channel.channel.volume_midi_cc)
983 self.entry_balance_cc.set_value(self.channel.channel.balance_midi_cc)
984 self.entry_mute_cc.set_value(self.channel.channel.mute_midi_cc)
985 if (self.channel and isinstance(self.channel, InputChannel)):
986 self.entry_solo_cc.set_value(self.channel.channel.solo_midi_cc)
988 def sense_popup_dialog(self, entry):
989 window = Gtk.Window.new(Gtk.WindowType.TOPLEVEL)
990 window.set_destroy_with_parent(True)
991 window.set_transient_for(self)
992 window.set_decorated(False)
993 window.set_modal(True)
994 window.set_position(Gtk.WindowPosition.CENTER_ON_PARENT)
995 window.set_border_width(10)
997 vbox = Gtk.Box(10, orientation=Gtk.Orientation.VERTICAL)
1000 vbox.pack_start(Gtk.Label(label='Please move the MIDI control you want to use for this function.'), True, True, 0)
1001 timeout_label = Gtk.Label(label='This window will close in 5 seconds')
1002 vbox.pack_start(timeout_label, True, True, 0)
1003 def close_sense_timeout(window, entry):
1005 timeout_label.set_text('This window will close in %d seconds.' % window.timeout)
1006 if window.timeout == 0:
1008 entry.set_value(self.mixer.last_midi_channel)
1012 GObject.timeout_add_seconds(1, close_sense_timeout, window, entry)
1014 def on_sense_midi_volume_clicked(self, *args):
1015 self.mixer.last_midi_channel = int(self.entry_volume_cc.get_value())
1016 self.sense_popup_dialog(self.entry_volume_cc)
1018 def on_sense_midi_balance_clicked(self, *args):
1019 self.mixer.last_midi_channel = int(self.entry_balance_cc.get_value())
1020 self.sense_popup_dialog(self.entry_balance_cc)
1022 def on_sense_midi_mute_clicked(self, *args):
1023 self.mixer.last_midi_channel = int(self.entry_mute_cc.get_value())
1024 self.sense_popup_dialog(self.entry_mute_cc)
1026 def on_sense_midi_solo_clicked(self, *args):
1027 self.mixer.last_midi_channel = int(self.entry_solo_cc.get_value())
1028 self.sense_popup_dialog(self.entry_solo_cc)
1030 def on_response_cb(self, dlg, response_id, *args):
1031 self.channel.channel_properties_dialog = None
1032 name = self.entry_name.get_text()
1033 if response_id == Gtk.ResponseType.APPLY:
1034 if name != self.channel.channel_name:
1035 self.channel.channel_name = name
1036 for control in ('volume', 'balance', 'mute', 'solo'):
1037 widget = getattr(self, 'entry_{}_cc'.format(control), None)
1038 if widget is not None:
1039 value = int(widget.get_value())
1041 setattr(self.channel.channel, '{}_midi_cc'.format(control), value)
1044 def on_entry_name_changed(self, entry):
1046 if len(entry.get_text()):
1047 if self.channel and self.channel.channel.name == entry.get_text():
1049 elif entry.get_text() not in [x.channel.name for x in self.app.channels] + \
1050 [x.channel.name for x in self.app.output_channels] + ['MAIN']:
1052 self.ok_button.set_sensitive(sensitive)
1055 GObject.signal_new("output-channel-order-changed", OutputChannel,
1056 GObject.SignalFlags.RUN_FIRST | GObject.SignalFlags.ACTION,
1057 None, [GObject.TYPE_STRING, GObject.TYPE_STRING])
1060 class NewChannelDialog(ChannelPropertiesDialog):
1061 def create_ui(self):
1062 ChannelPropertiesDialog.create_ui(self)
1063 self.add_initial_value_radio()
1064 self.vbox.show_all()
1066 def add_initial_value_radio(self):
1067 grid = self.properties_grid
1068 grid.attach(Gtk.Label(label='Value', halign=Gtk.Align.START), 0, 2, 1, 1)
1069 self.minus_inf = Gtk.RadioButton.new_with_mnemonic(None, '-_Inf')
1070 self.zero_dB = Gtk.RadioButton.new_with_mnemonic_from_widget(self.minus_inf, '_0dB')
1071 grid.attach(self.minus_inf, 1, 2, 1, 1)
1072 grid.attach(self.zero_dB, 2, 2, 1, 1)
1075 class NewInputChannelDialog(NewChannelDialog):
1076 def __init__(self, app):
1077 Gtk.Dialog.__init__(self, 'New Input Channel', app.window)
1078 self.set_default_size(365, -1)
1079 self.mixer = app.mixer
1083 self.add_button(Gtk.STOCK_CANCEL, Gtk.ResponseType.CANCEL)
1084 self.ok_button = self.add_button(Gtk.STOCK_ADD, Gtk.ResponseType.OK)
1085 self.ok_button.set_sensitive(False)
1086 self.set_default_response(Gtk.ResponseType.OK);
1088 def fill_ui(self, **values):
1089 self.entry_name.set_text(values.get('name', ''))
1090 # don't set MIDI CCs to previously used values, because they
1091 # would overwrite existing mappings, if accepted.
1092 self.entry_volume_cc.set_value(-1)
1093 self.entry_balance_cc.set_value(-1)
1094 self.entry_mute_cc.set_value(-1)
1095 self.entry_solo_cc.set_value(-1)
1096 self.stereo.set_active(values.get('stereo', True))
1097 self.minus_inf.set_active(values.get('value', False))
1098 self.entry_name.grab_focus()
1100 def get_result(self):
1102 'name': self.entry_name.get_text(),
1103 'stereo': self.stereo.get_active(),
1104 'volume_cc': int(self.entry_volume_cc.get_value()),
1105 'balance_cc': int(self.entry_balance_cc.get_value()),
1106 'mute_cc': int(self.entry_mute_cc.get_value()),
1107 'solo_cc': int(self.entry_solo_cc.get_value()),
1108 'value': self.minus_inf.get_active()
1112 class OutputChannelPropertiesDialog(ChannelPropertiesDialog):
1113 def create_ui(self):
1114 ChannelPropertiesDialog.create_ui(self)
1116 grid = self.properties_grid
1117 color_label = Gtk.Label.new_with_mnemonic('_Color')
1118 color_label.set_halign(Gtk.Align.START)
1119 grid.attach(color_label, 0, 3, 1, 1)
1120 self.color_chooser_button = Gtk.ColorButton()
1121 self.color_chooser_button.set_use_alpha(True)
1122 self.color_chooser_button.set_rgba(Gdk.RGBA(0, 0, 0, 0))
1123 color_label.set_mnemonic_widget(self.color_chooser_button)
1124 grid.attach(self.color_chooser_button, 1, 3, 2, 1)
1126 vbox = Gtk.Box(orientation=Gtk.Orientation.VERTICAL)
1127 self.vbox.pack_start(self.create_frame('Input Channels', vbox), True, True, 0)
1129 self.display_solo_buttons = Gtk.CheckButton.new_with_mnemonic('_Display solo buttons')
1130 vbox.pack_start(self.display_solo_buttons, True, True, 0)
1132 self.vbox.show_all()
1135 ChannelPropertiesDialog.fill_ui(self)
1136 self.display_solo_buttons.set_active(self.channel.display_solo_buttons)
1137 self.color_chooser_button.set_rgba(self.channel.color)
1139 def on_response_cb(self, dlg, response_id, *args):
1140 ChannelPropertiesDialog.on_response_cb(self, dlg, response_id, *args)
1141 if response_id == Gtk.ResponseType.APPLY:
1142 self.channel.display_solo_buttons = self.display_solo_buttons.get_active()
1143 self.channel.set_color(self.color_chooser_button.get_rgba())
1144 for inputchannel in self.app.channels:
1145 inputchannel.update_control_group(self.channel)
1148 class NewOutputChannelDialog(NewChannelDialog, OutputChannelPropertiesDialog):
1149 def __init__(self, app):
1150 Gtk.Dialog.__init__(self, 'New Output Channel', app.window)
1151 self.mixer = app.mixer
1153 OutputChannelPropertiesDialog.create_ui(self)
1154 self.add_initial_value_radio()
1155 self.vbox.show_all()
1156 self.set_default_size(365, -1)
1158 # TODO: disable mode for output channels as mono output channels may
1159 # not be correctly handled yet.
1160 self.mono.set_sensitive(False)
1161 self.stereo.set_sensitive(False)
1163 self.add_button(Gtk.STOCK_CANCEL, Gtk.ResponseType.CANCEL)
1164 self.ok_button = self.add_button(Gtk.STOCK_ADD, Gtk.ResponseType.OK)
1165 self.ok_button.set_sensitive(False)
1166 self.set_default_response(Gtk.ResponseType.OK);
1168 def fill_ui(self, **values):
1169 self.entry_name.set_text(values.get('name', ''))
1170 # don't set MIDI CCs to previously used values, because they
1171 # would overwrite existing mappings, if accepted.
1172 self.entry_volume_cc.set_value(-1)
1173 self.entry_balance_cc.set_value(-1)
1174 self.entry_mute_cc.set_value(-1)
1175 self.stereo.set_active(values.get('stereo', True))
1176 self.minus_inf.set_active(values.get('value', False))
1177 self.color_chooser_button.set_rgba(values.get('color', Gdk.RGBA(0, 0, 0, 0)))
1178 self.display_solo_buttons.set_active(values.get('display_solo_buttons', False))
1179 self.entry_name.grab_focus()
1181 def get_result(self):
1183 'name': self.entry_name.get_text(),
1184 'stereo': self.stereo.get_active(),
1185 'volume_cc': int(self.entry_volume_cc.get_value()),
1186 'balance_cc': int(self.entry_balance_cc.get_value()),
1187 'mute_cc': int(self.entry_mute_cc.get_value()),
1188 'display_solo_buttons': self.display_solo_buttons.get_active(),
1189 'color': self.color_chooser_button.get_rgba(),
1190 'value': self.minus_inf.get_active()
1194 class ControlGroup(Gtk.Alignment):
1195 def __init__(self, output_channel, input_channel):
1196 GObject.GObject.__init__(self)
1197 self.set(0.5, 0.5, 1, 1)
1198 self.output_channel = output_channel
1199 self.input_channel = input_channel
1200 self.app = input_channel.app
1202 self.hbox = Gtk.Box(orientation=Gtk.Orientation.HORIZONTAL)
1203 self.vbox = Gtk.Box(orientation=Gtk.Orientation.VERTICAL)
1205 self.buttons_box = Gtk.Box(False, button_padding, orientation=Gtk.Orientation.HORIZONTAL)
1207 set_background_color(self.vbox, output_channel.css_name, output_channel.color)
1209 self.vbox.pack_start(self.hbox, True, True, button_padding)
1216 .control_group #label,
1217 .control_group #mute,
1218 .control_group #pre_fader,
1219 .control_group #solo {
1225 css_provider = Gtk.CssProvider()
1226 css_provider.load_from_data(css)
1227 context = Gtk.StyleContext()
1228 screen = Gdk.Screen.get_default()
1229 context.add_provider_for_screen(screen, css_provider, Gtk.STYLE_PROVIDER_PRIORITY_APPLICATION)
1230 hbox_context = self.hbox.get_style_context()
1231 hbox_context.add_class('control_group')
1233 name = output_channel.channel.name
1234 self.label = Gtk.Label(name)
1235 self.label.set_name("label")
1236 self.label.set_max_width_chars(self.input_channel.label_chars_narrow)
1237 self.label.set_ellipsize(Pango.EllipsizeMode.MIDDLE)
1238 if len(name) > self.input_channel.label_chars_narrow:
1239 self.label.set_tooltip_text(name)
1240 self.hbox.pack_start(self.label, False, False, button_padding)
1241 self.hbox.pack_end(self.buttons_box, False, False, button_padding)
1242 mute = Gtk.ToggleButton()
1244 mute.set_name("mute")
1245 mute.set_tooltip_text("Mute output channel send")
1246 mute.connect("toggled", self.on_mute_toggled)
1248 solo = Gtk.ToggleButton()
1249 solo.set_name("solo")
1251 solo.set_tooltip_text("Solo output send")
1252 solo.connect("toggled", self.on_solo_toggled)
1254 pre = Gtk.ToggleButton("P")
1255 pre.set_name("pre_fader")
1256 pre.set_tooltip_text("Pre (on) / Post (off) fader send")
1257 pre.connect("toggled", self.on_prefader_toggled)
1259 self.buttons_box.pack_start(pre, True, True, button_padding)
1260 self.buttons_box.pack_start(mute, True, True, button_padding)
1261 if self.output_channel.display_solo_buttons:
1262 self.buttons_box.pack_start(solo, True, True, button_padding)
1265 if self.output_channel.display_solo_buttons:
1266 if not self.solo in self.buttons_box.get_children():
1267 self.buttons_box.pack_start(self.solo, True, True, button_padding)
1270 if self.solo in self.buttons_box.get_children():
1271 self.buttons_box.remove(self.solo)
1273 name = self.output_channel.channel.name
1274 self.label.set_text(name)
1275 if len(name) > self.input_channel.label_chars_narrow:
1276 self.label.set_tooltip_text(name)
1278 set_background_color(self.vbox, self.output_channel.css_name, self.output_channel.color)
1280 def on_mute_toggled(self, button):
1281 self.output_channel.channel.set_muted(self.input_channel.channel, button.get_active())
1282 self.app.update_monitor(self)
1284 def on_solo_toggled(self, button):
1285 self.output_channel.channel.set_solo(self.input_channel.channel, button.get_active())
1286 self.app.update_monitor(self)
1288 def on_prefader_toggled(self, button):
1289 self.output_channel.channel.set_in_prefader(self.input_channel.channel, button.get_active())
1292 self.hbox.remove(self.label)
1293 self.hbox.set_child_packing(self.buttons_box, True, True, button_padding, Gtk.PackType.END)
1296 self.hbox.pack_start(self.label, False, False, button_padding)
1297 self.hbox.set_child_packing(self.buttons_box, False, False, button_padding, Gtk.PackType.END)