Set default filename for file chooser for save-as if known
[jack_mixer.git] / jack_mixer.py
blobd539c4bf7ebf5fc4b0fb8973b4bbcc105840a3c5
1 #!/usr/bin/env python3
2 # -*- coding: utf-8 -*-
4 # This file is part of jack_mixer
6 # Copyright (C) 2006-2009 Nedko Arnaudov <nedko@arnaudov.name>
7 # Copyright (C) 2009-2020 Frederic Peters <fpeters@0d.be> et al.
9 # This program is free software; you can redistribute it and/or modify
10 # it under the terms of the GNU General Public License as published by
11 # the Free Software Foundation; version 2 of the License
13 # This program is distributed in the hope that it will be useful,
14 # but WITHOUT ANY WARRANTY; without even the implied warranty of
15 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
16 # GNU General Public License for more details.
18 # You should have received a copy of the GNU General Public License
19 # along with this program; if not, write to the Free Software
20 # Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA.
22 import logging
23 import os
24 import re
25 import signal
26 import sys
27 from argparse import ArgumentParser
29 import gi
31 gi.require_version("Gtk", "3.0")
32 from gi.repository import Gtk
33 from gi.repository import GLib
35 # temporary change Python modules lookup path to look into installation
36 # directory ($prefix/share/jack_mixer/)
37 old_path = sys.path
38 sys.path.insert(0, os.path.join(os.path.dirname(sys.argv[0]), "..", "share", "jack_mixer"))
40 import jack_mixer_c
42 import gui
43 import scale
44 from channel import InputChannel, NewInputChannelDialog, NewOutputChannelDialog, OutputChannel
45 from nsmclient import NSMClient
46 from serialization_xml import XmlSerialization
47 from serialization import SerializedObject, Serializator
48 from styling import load_css_styles
49 from preferences import PreferencesDialog
50 from version import __version__
52 # restore Python modules lookup path
53 sys.path = old_path
54 log = logging.getLogger("jack_mixer")
57 def add_number_suffix(s):
58 def inc(match):
59 return str(int(match.group(0)) + 1)
61 new_s = re.sub(r"(\d+)\s*$", inc, s)
62 if new_s == s:
63 new_s = s + " 1"
65 return new_s
68 class JackMixer(SerializedObject):
70 # scales suitable as meter scales
71 meter_scales = [
72 scale.K20(),
73 scale.K14(),
74 scale.IEC268(),
75 scale.Linear70dB(),
76 scale.IEC268Minimalistic(),
79 # scales suitable as volume slider scales
80 slider_scales = [scale.Linear30dB(), scale.Linear70dB()]
82 def __init__(self, client_name="jack_mixer"):
83 self.visible = False
84 self.nsm_client = None
85 # name of settings file that is currently open
86 self.current_filename = None
87 self._monitored_channel = None
88 self._init_solo_channels = None
90 if os.environ.get("NSM_URL"):
91 self.nsm_client = NSMClient(
92 prettyName="jack_mixer",
93 saveCallback=self.nsm_save_cb,
94 openOrNewCallback=self.nsm_open_cb,
95 supportsSaveStatus=False,
96 hideGUICallback=self.nsm_hide_cb,
97 showGUICallback=self.nsm_show_cb,
98 exitProgramCallback=self.nsm_exit_cb,
99 loggingLevel="error",
101 self.nsm_client.announceGuiVisibility(self.visible)
102 else:
103 self.visible = True
104 self.create_mixer(client_name, with_nsm=False)
106 def create_mixer(self, client_name, with_nsm=True):
107 self.mixer = jack_mixer_c.Mixer(client_name)
108 if not self.mixer:
109 raise RuntimeError("Failed to create Mixer instance.")
111 self.create_ui(with_nsm)
112 self.window.set_title(client_name)
114 self.monitor_channel = self.mixer.add_output_channel("Monitor", True, True)
116 GLib.timeout_add(33, self.read_meters)
117 GLib.timeout_add(50, self.midi_events_check)
119 if with_nsm:
120 GLib.timeout_add(200, self.nsm_react)
122 def cleanup(self):
123 log.debug("Cleaning jack_mixer.")
124 if not self.mixer:
125 return
127 for channel in self.channels:
128 channel.unrealize()
130 self.mixer.destroy()
132 # ---------------------------------------------------------------------------------------------
133 # UI creation and (de-)initialization
135 def new_menu_item(self, title, callback=None, accel=None, enabled=True):
136 menuitem = Gtk.MenuItem.new_with_mnemonic(title)
137 menuitem.set_sensitive(enabled)
138 if callback:
139 menuitem.connect("activate", callback)
140 if accel:
141 key, mod = Gtk.accelerator_parse(accel)
142 menuitem.add_accelerator(
143 "activate", self.menu_accelgroup, key, mod, Gtk.AccelFlags.VISIBLE
145 return menuitem
147 def create_ui(self, with_nsm):
148 self.channels = []
149 self.output_channels = []
150 load_css_styles()
151 self.window = Gtk.Window(type=Gtk.WindowType.TOPLEVEL)
152 self.window.set_icon_name("jack_mixer")
153 self.gui_factory = gui.Factory(self.window, self.meter_scales, self.slider_scales)
154 self.gui_factory.connect("midi-behavior-mode-changed", self.on_midi_behavior_mode_changed)
155 self.gui_factory.emit_midi_behavior_mode()
157 self.vbox_top = Gtk.VBox()
158 self.window.add(self.vbox_top)
160 self.menu_accelgroup = Gtk.AccelGroup()
161 self.window.add_accel_group(self.menu_accelgroup)
163 self.menubar = Gtk.MenuBar()
164 self.vbox_top.pack_start(self.menubar, False, True, 0)
166 mixer_menu_item = Gtk.MenuItem.new_with_mnemonic("_Mixer")
167 self.menubar.append(mixer_menu_item)
168 edit_menu_item = Gtk.MenuItem.new_with_mnemonic("_Edit")
169 self.menubar.append(edit_menu_item)
170 help_menu_item = Gtk.MenuItem.new_with_mnemonic("_Help")
171 self.menubar.append(help_menu_item)
173 self.width = 420
174 self.height = 420
175 self.paned_position = 210
176 self.window.set_default_size(self.width, self.height)
178 self.mixer_menu = Gtk.Menu()
179 mixer_menu_item.set_submenu(self.mixer_menu)
181 self.mixer_menu.append(
182 self.new_menu_item("New _Input Channel", self.on_add_input_channel, "<Control>N")
184 self.mixer_menu.append(
185 self.new_menu_item(
186 "New Output _Channel", self.on_add_output_channel, "<Shift><Control>N"
190 self.mixer_menu.append(Gtk.SeparatorMenuItem())
191 if not with_nsm:
192 self.mixer_menu.append(self.new_menu_item("_Open...", self.on_open_cb, "<Control>O"))
194 self.mixer_menu.append(self.new_menu_item("_Save", self.on_save_cb, "<Control>S"))
196 if not with_nsm:
197 self.mixer_menu.append(
198 self.new_menu_item("Save _As...", self.on_save_as_cb, "<Shift><Control>S")
201 self.mixer_menu.append(Gtk.SeparatorMenuItem())
202 if with_nsm:
203 self.mixer_menu.append(self.new_menu_item("_Hide", self.nsm_hide_cb, "<Control>W"))
204 else:
205 self.mixer_menu.append(self.new_menu_item("_Quit", self.on_quit_cb, "<Control>Q"))
207 edit_menu = Gtk.Menu()
208 edit_menu_item.set_submenu(edit_menu)
210 self.channel_edit_input_menu_item = self.new_menu_item(
211 "_Edit Input Channel", enabled=False
213 edit_menu.append(self.channel_edit_input_menu_item)
214 self.channel_edit_input_menu = Gtk.Menu()
215 self.channel_edit_input_menu_item.set_submenu(self.channel_edit_input_menu)
217 self.channel_edit_output_menu_item = self.new_menu_item(
218 "E_dit Output Channel", enabled=False
220 edit_menu.append(self.channel_edit_output_menu_item)
221 self.channel_edit_output_menu = Gtk.Menu()
222 self.channel_edit_output_menu_item.set_submenu(self.channel_edit_output_menu)
224 self.channel_remove_input_menu_item = self.new_menu_item(
225 "_Remove Input Channel", enabled=False
227 edit_menu.append(self.channel_remove_input_menu_item)
228 self.channel_remove_input_menu = Gtk.Menu()
229 self.channel_remove_input_menu_item.set_submenu(self.channel_remove_input_menu)
231 self.channel_remove_output_menu_item = self.new_menu_item(
232 "Re_move Output Channel", enabled=False
234 edit_menu.append(self.channel_remove_output_menu_item)
235 self.channel_remove_output_menu = Gtk.Menu()
236 self.channel_remove_output_menu_item.set_submenu(self.channel_remove_output_menu)
238 edit_menu.append(Gtk.SeparatorMenuItem())
239 edit_menu.append(
240 self.new_menu_item("Shrink Channels", self.on_shrink_channels_cb, "<Control>minus")
242 edit_menu.append(
243 self.new_menu_item("Expand Channels", self.on_expand_channels_cb, "<Control>plus")
245 edit_menu.append(Gtk.SeparatorMenuItem())
247 edit_menu.append(self.new_menu_item("_Clear", self.on_channels_clear, "<Control>X"))
248 edit_menu.append(Gtk.SeparatorMenuItem())
250 self.preferences_dialog = None
251 edit_menu.append(self.new_menu_item("_Preferences", self.on_preferences_cb, "<Control>P"))
253 help_menu = Gtk.Menu()
254 help_menu_item.set_submenu(help_menu)
256 help_menu.append(self.new_menu_item("_About", self.on_about, "F1"))
258 self.hbox_top = Gtk.HBox()
259 self.vbox_top.pack_start(self.hbox_top, True, True, 0)
261 self.scrolled_window = Gtk.ScrolledWindow()
262 self.scrolled_window.set_policy(Gtk.PolicyType.AUTOMATIC, Gtk.PolicyType.AUTOMATIC)
264 self.hbox_inputs = Gtk.Box()
265 self.hbox_inputs.set_spacing(0)
266 self.hbox_inputs.set_border_width(0)
267 self.hbox_top.set_spacing(0)
268 self.hbox_top.set_border_width(0)
269 self.scrolled_window.add(self.hbox_inputs)
270 self.hbox_outputs = Gtk.Box()
271 self.hbox_outputs.set_spacing(0)
272 self.hbox_outputs.set_border_width(0)
273 self.scrolled_output = Gtk.ScrolledWindow()
274 self.scrolled_output.set_policy(Gtk.PolicyType.AUTOMATIC, Gtk.PolicyType.AUTOMATIC)
275 self.scrolled_output.add(self.hbox_outputs)
276 self.paned = Gtk.HPaned()
277 self.paned.set_wide_handle(True)
278 self.hbox_top.pack_start(self.paned, True, True, 0)
279 self.paned.pack1(self.scrolled_window, True, False)
280 self.paned.pack2(self.scrolled_output, True, False)
282 self.window.connect("destroy", Gtk.main_quit)
283 self.window.connect("delete-event", self.on_delete_event)
285 # ---------------------------------------------------------------------------------------------
286 # Channel creation
288 def add_channel(self, name, stereo, volume_cc, balance_cc, mute_cc, solo_cc, value):
289 try:
290 channel = InputChannel(self, name, stereo, value)
291 self.add_channel_precreated(channel)
292 except Exception:
293 error_dialog(self.window, "Input channel creation failed.")
294 return
296 channel.assign_midi_ccs(volume_cc, balance_cc, mute_cc, solo_cc)
297 return channel
299 def add_channel_precreated(self, channel):
300 frame = Gtk.Frame()
301 frame.add(channel)
302 self.hbox_inputs.pack_start(frame, False, True, 0)
303 channel.realize()
305 channel_edit_menu_item = Gtk.MenuItem(label=channel.channel_name)
306 self.channel_edit_input_menu.append(channel_edit_menu_item)
307 channel_edit_menu_item.connect("activate", self.on_edit_input_channel, channel)
308 self.channel_edit_input_menu_item.set_sensitive(True)
310 channel_remove_menu_item = Gtk.MenuItem(label=channel.channel_name)
311 self.channel_remove_input_menu.append(channel_remove_menu_item)
312 channel_remove_menu_item.connect("activate", self.on_remove_input_channel, channel)
313 self.channel_remove_input_menu_item.set_sensitive(True)
315 self.channels.append(channel)
317 for outputchannel in self.output_channels:
318 channel.add_control_group(outputchannel)
320 # create post fader output channel matching the input channel
321 channel.post_fader_output_channel = self.mixer.add_output_channel(
322 channel.channel.name + " Out", channel.channel.is_stereo, True
324 channel.post_fader_output_channel.volume = 0
325 channel.post_fader_output_channel.set_solo(channel.channel, True)
327 channel.connect("input-channel-order-changed", self.on_input_channel_order_changed)
329 def add_output_channel(
330 self, name, stereo, volume_cc, balance_cc, mute_cc, display_solo_buttons, color, value
332 try:
333 channel = OutputChannel(self, name, stereo, value)
334 channel.display_solo_buttons = display_solo_buttons
335 channel.color = color
336 self.add_output_channel_precreated(channel)
337 except Exception:
338 error_dialog(self.window, "Output channel creation failed")
339 return
341 channel.assign_midi_ccs(volume_cc, balance_cc, mute_cc)
342 return channel
344 def add_output_channel_precreated(self, channel):
345 frame = Gtk.Frame()
346 frame.add(channel)
347 self.hbox_outputs.pack_end(frame, False, True, 0)
348 self.hbox_outputs.reorder_child(frame, 0)
349 channel.realize()
351 channel_edit_menu_item = Gtk.MenuItem(label=channel.channel_name)
352 self.channel_edit_output_menu.append(channel_edit_menu_item)
353 channel_edit_menu_item.connect("activate", self.on_edit_output_channel, channel)
354 self.channel_edit_output_menu_item.set_sensitive(True)
356 channel_remove_menu_item = Gtk.MenuItem(label=channel.channel_name)
357 self.channel_remove_output_menu.append(channel_remove_menu_item)
358 channel_remove_menu_item.connect("activate", self.on_remove_output_channel, channel)
359 self.channel_remove_output_menu_item.set_sensitive(True)
361 self.output_channels.append(channel)
362 channel.connect("output-channel-order-changed", self.on_output_channel_order_changed)
364 # ---------------------------------------------------------------------------------------------
365 # Signal/event handlers
367 # ---------------------------------------------------------------------------------------------
368 # NSM
370 def nsm_react(self):
371 self.nsm_client.reactToMessage()
372 return True
374 def nsm_hide_cb(self, *args):
375 self.window.hide()
376 self.visible = False
377 self.nsm_client.announceGuiVisibility(False)
379 def nsm_show_cb(self):
380 width, height = self.window.get_size()
381 self.window.show_all()
382 self.paned.set_position(self.paned_position / self.width * width)
384 self.visible = True
385 self.nsm_client.announceGuiVisibility(True)
387 def nsm_open_cb(self, path, session_name, client_name):
388 self.create_mixer(client_name, with_nsm=True)
389 self.current_filename = path + ".xml"
390 if os.path.isfile(self.current_filename):
391 try:
392 with open(self.current_filename, "r") as fp:
393 self.load_from_xml(fp, from_nsm=True)
394 except Exception as exc:
395 # Re-raise with more meaningful error message
396 raise IOError(
397 "Error loading settings file '{}': {}".format(self.current_filename, exc)
400 def nsm_save_cb(self, path, session_name, client_name):
401 self.current_filename = path + ".xml"
402 f = open(self.current_filename, "w")
403 self.save_to_xml(f)
404 f.close()
406 def nsm_exit_cb(self, path, session_name, client_name):
407 Gtk.main_quit()
409 # ---------------------------------------------------------------------------------------------
410 # POSIX signals
412 def sighandler(self, signum, frame):
413 log.debug("Signal %d received.", signum)
414 if signum == signal.SIGUSR1:
415 GLib.timeout_add(0, self.on_save_cb)
416 elif signum == signal.SIGINT or signal == signal.SIGTERM:
417 GLib.timeout_add(0, self.on_quit_cb)
418 else:
419 log.warning("Unknown signal %d received.", signum)
421 # ---------------------------------------------------------------------------------------------
422 # GTK signals
424 def on_about(self, *args):
425 about = Gtk.AboutDialog()
426 about.set_name("jack_mixer")
427 about.set_program_name("jack_mixer")
428 about.set_copyright(
429 "Copyright © 2006-2020\n"
430 "Nedko Arnaudov, Frédéric Péters, Arnout Engelen, Daniel Sheeler"
432 about.set_license(
433 """\
434 jack_mixer is free software; you can redistribute it and/or modify it
435 under the terms of the GNU General Public License as published by the
436 Free Software Foundation; either version 2 of the License, or (at your
437 option) any later version.
439 jack_mixer is distributed in the hope that it will be useful, but
440 WITHOUT ANY WARRANTY; without even the implied warranty of
441 MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
442 General Public License for more details.
444 You should have received a copy of the GNU General Public License along
445 with jack_mixer; if not, write to the Free Software Foundation, Inc., 51
446 Franklin Street, Fifth Floor, Boston, MA 02110-130159 USA"""
448 about.set_authors(
450 "Nedko Arnaudov <nedko@arnaudov.name>",
451 "Christopher Arndt <chris@chrisarndt.de>",
452 "Arnout Engelen <arnouten@bzzt.net>",
453 "John Hedges <john@drystone.co.uk>",
454 "Olivier Humbert <trebmuh@tuxfamily.org>",
455 "Sarah Mischke <sarah@spooky-online.de>",
456 "Frédéric Péters <fpeters@0d.be>",
457 "Daniel Sheeler <dsheeler@pobox.com>",
458 "Athanasios Silis <athanasios.silis@gmail.com>",
461 about.set_logo_icon_name("jack_mixer")
462 about.set_version(__version__)
463 about.set_website("https://rdio.space/jackmixer/")
464 about.run()
465 about.destroy()
467 def on_delete_event(self, widget, event):
468 if self.nsm_client:
469 self.nsm_hide_cb()
470 return True
472 return self.on_quit_cb(on_delete=True)
474 def on_open_cb(self, *args):
475 dlg = Gtk.FileChooserDialog(
476 title="Open", parent=self.window, action=Gtk.FileChooserAction.OPEN
478 dlg.add_buttons(
479 Gtk.STOCK_CANCEL, Gtk.ResponseType.CANCEL, Gtk.STOCK_OPEN, Gtk.ResponseType.OK
481 dlg.set_default_response(Gtk.ResponseType.OK)
482 if dlg.run() == Gtk.ResponseType.OK:
483 filename = dlg.get_filename()
484 try:
485 with open(filename, "r") as fp:
486 self.load_from_xml(fp)
487 except Exception as exc:
488 error_dialog(self.window, "Error loading settings file '%s': %s", filename, exc)
489 else:
490 self.current_filename = filename
491 dlg.destroy()
493 def on_save_cb(self, *args):
494 if not self.current_filename:
495 return self.on_save_as_cb()
496 f = open(self.current_filename, "w")
497 self.save_to_xml(f)
498 f.close()
500 def on_save_as_cb(self, *args):
501 dlg = Gtk.FileChooserDialog(
502 title="Save", parent=self.window, action=Gtk.FileChooserAction.SAVE
504 dlg.add_buttons(
505 Gtk.STOCK_CANCEL, Gtk.ResponseType.CANCEL, Gtk.STOCK_SAVE, Gtk.ResponseType.OK
507 dlg.set_default_response(Gtk.ResponseType.OK)
509 if self.current_filename:
510 dlg.set_filename(self.current_filename)
512 if dlg.run() == Gtk.ResponseType.OK:
513 self.current_filename = dlg.get_filename()
514 self.on_save_cb()
515 dlg.destroy()
517 def on_quit_cb(self, *args, on_delete=False):
518 if not self.nsm_client and self.gui_factory.get_confirm_quit():
519 dlg = Gtk.MessageDialog(
520 parent=self.window,
521 message_type=Gtk.MessageType.QUESTION,
522 buttons=Gtk.ButtonsType.NONE,
524 dlg.set_markup("<b>Quit application?</b>")
525 dlg.format_secondary_markup(
526 "All jack_mixer ports will be closed and connections lost,"
527 "\nstopping all sound going through jack_mixer.\n\n"
528 "Are you sure?"
530 dlg.add_buttons(
531 Gtk.STOCK_CANCEL, Gtk.ResponseType.CANCEL, Gtk.STOCK_QUIT, Gtk.ResponseType.OK
533 response = dlg.run()
534 dlg.destroy()
535 if response != Gtk.ResponseType.OK:
536 return on_delete
538 Gtk.main_quit()
540 def on_shrink_channels_cb(self, widget):
541 for channel in self.channels + self.output_channels:
542 channel.narrow()
544 def on_expand_channels_cb(self, widget):
545 for channel in self.channels + self.output_channels:
546 channel.widen()
548 def on_midi_behavior_mode_changed(self, gui_factory, value):
549 self.mixer.midi_behavior_mode = value
551 def on_preferences_cb(self, widget):
552 if not self.preferences_dialog:
553 self.preferences_dialog = PreferencesDialog(self)
554 self.preferences_dialog.show()
555 self.preferences_dialog.present()
557 def on_add_channel(self, inout="input", default_name="Input"):
558 dialog = getattr(self, "_add_{}_dialog".format(inout), None)
559 values = getattr(self, "_add_{}_values".format(inout), {})
561 if dialog is None:
562 cls = NewInputChannelDialog if inout == "input" else NewOutputChannelDialog
563 dialog = cls(app=self)
564 setattr(self, "_add_{}_dialog".format(inout), dialog)
566 names = {
567 ch.channel_name for ch in (self.channels if inout == "input" else self.output_channels)
569 values.setdefault("name", default_name)
570 while True:
571 if values["name"] in names:
572 values["name"] = add_number_suffix(values["name"])
573 else:
574 break
576 dialog.fill_ui(**values)
577 dialog.set_transient_for(self.window)
578 dialog.show()
579 ret = dialog.run()
580 dialog.hide()
582 if ret == Gtk.ResponseType.OK:
583 result = dialog.get_result()
584 setattr(self, "_add_{}_values".format(inout), result)
585 (self.add_channel if inout == "input" else self.add_output_channel)(**result)
586 if self.visible or self.nsm_client is None:
587 self.window.show_all()
589 def on_add_input_channel(self, widget):
590 return self.on_add_channel("input", "Input")
592 def on_add_output_channel(self, widget):
593 return self.on_add_channel("output", "Output")
595 def on_edit_input_channel(self, widget, channel):
596 log.debug('Editing input channel "%s".', channel.channel_name)
597 channel.on_channel_properties()
599 def on_remove_input_channel(self, widget, channel):
600 log.debug('Removing input channel "%s".', channel.channel_name)
602 def remove_channel_edit_input_menuitem_by_label(widget, label):
603 if widget.get_label() == label:
604 self.channel_edit_input_menu.remove(widget)
606 self.channel_remove_input_menu.remove(widget)
607 self.channel_edit_input_menu.foreach(
608 remove_channel_edit_input_menuitem_by_label, channel.channel_name
611 if self.monitored_channel is channel:
612 channel.monitor_button.set_active(False)
614 for i in range(len(self.channels)):
615 if self.channels[i] is channel:
616 channel.unrealize()
617 del self.channels[i]
618 self.hbox_inputs.remove(channel.get_parent())
619 break
621 if not self.channels:
622 self.channel_edit_input_menu_item.set_sensitive(False)
623 self.channel_remove_input_menu_item.set_sensitive(False)
625 def on_edit_output_channel(self, widget, channel):
626 log.debug('Editing output channel "%s".', channel.channel_name)
627 channel.on_channel_properties()
629 def on_remove_output_channel(self, widget, channel):
630 log.debug('Removing output channel "%s".', channel.channel_name)
632 def remove_channel_edit_output_menuitem_by_label(widget, label):
633 if widget.get_label() == label:
634 self.channel_edit_output_menu.remove(widget)
636 self.channel_remove_output_menu.remove(widget)
637 self.channel_edit_output_menu.foreach(
638 remove_channel_edit_output_menuitem_by_label, channel.channel_name
641 if self.monitored_channel is channel:
642 channel.monitor_button.set_active(False)
644 for i in range(len(self.channels)):
645 if self.output_channels[i] is channel:
646 channel.unrealize()
647 del self.output_channels[i]
648 self.hbox_outputs.remove(channel.get_parent())
649 break
651 if not self.output_channels:
652 self.channel_edit_output_menu_item.set_sensitive(False)
653 self.channel_remove_output_menu_item.set_sensitive(False)
655 def on_channel_rename(self, oldname, newname):
656 def rename_channels(container, parameters):
657 if container.get_label() == parameters["oldname"]:
658 container.set_label(parameters["newname"])
660 rename_parameters = {"oldname": oldname, "newname": newname}
661 self.channel_edit_input_menu.foreach(rename_channels, rename_parameters)
662 self.channel_edit_output_menu.foreach(rename_channels, rename_parameters)
663 self.channel_remove_input_menu.foreach(rename_channels, rename_parameters)
664 self.channel_remove_output_menu.foreach(rename_channels, rename_parameters)
665 log.debug('Renaming channel from "%s" to "%s".', oldname, newname)
667 def on_input_channel_order_changed(self, widget, source_name, dest_name):
668 self.channels.clear()
670 channel_box = self.hbox_inputs
671 frames = channel_box.get_children()
673 for f in frames:
674 c = f.get_child()
675 if source_name == c._channel_name:
676 source_frame = f
677 break
679 for f in frames:
680 c = f.get_child()
681 if dest_name == c._channel_name:
682 pos = frames.index(f)
683 channel_box.reorder_child(source_frame, pos)
684 break
686 for frame in self.hbox_inputs.get_children():
687 c = frame.get_child()
688 self.channels.append(c)
690 def on_output_channel_order_changed(self, widget, source_name, dest_name):
691 self.output_channels.clear()
692 channel_box = self.hbox_outputs
694 frames = channel_box.get_children()
696 for f in frames:
697 c = f.get_child()
698 if source_name == c._channel_name:
699 source_frame = f
700 break
702 for f in frames:
703 c = f.get_child()
704 if dest_name == c._channel_name:
705 pos = len(frames) - 1 - frames.index(f)
706 channel_box.reorder_child(source_frame, pos)
707 break
709 for frame in self.hbox_outputs.get_children():
710 c = frame.get_child()
711 self.output_channels.append(c)
713 def on_channels_clear(self, widget):
714 dlg = Gtk.MessageDialog(
715 parent=self.window,
716 modal=True,
717 message_type=Gtk.MessageType.WARNING,
718 text="Are you sure you want to clear all channels?",
719 buttons=Gtk.ButtonsType.OK_CANCEL,
721 if not widget or dlg.run() == Gtk.ResponseType.OK:
722 for channel in self.output_channels:
723 channel.unrealize()
724 self.hbox_outputs.remove(channel.get_parent())
725 for channel in self.channels:
726 channel.unrealize()
727 self.hbox_inputs.remove(channel.get_parent())
728 self.channels = []
729 self.output_channels = []
730 self.channel_edit_input_menu = Gtk.Menu()
731 self.channel_edit_input_menu_item.set_submenu(self.channel_edit_input_menu)
732 self.channel_edit_input_menu_item.set_sensitive(False)
733 self.channel_remove_input_menu = Gtk.Menu()
734 self.channel_remove_input_menu_item.set_submenu(self.channel_remove_input_menu)
735 self.channel_remove_input_menu_item.set_sensitive(False)
736 self.channel_edit_output_menu = Gtk.Menu()
737 self.channel_edit_output_menu_item.set_submenu(self.channel_edit_output_menu)
738 self.channel_edit_output_menu_item.set_sensitive(False)
739 self.channel_remove_output_menu = Gtk.Menu()
740 self.channel_remove_output_menu_item.set_submenu(self.channel_remove_output_menu)
741 self.channel_remove_output_menu_item.set_sensitive(False)
742 dlg.destroy()
744 def read_meters(self):
745 for channel in self.channels:
746 channel.read_meter()
747 for channel in self.output_channels:
748 channel.read_meter()
749 return True
751 def midi_events_check(self):
752 for channel in self.channels + self.output_channels:
753 channel.midi_events_check()
754 return True
756 def get_monitored_channel(self):
757 return self._monitored_channel
759 def set_monitored_channel(self, channel):
760 if channel == self._monitored_channel:
761 return
762 self._monitored_channel = channel
763 if channel is None:
764 self.monitor_channel.out_mute = True
765 elif isinstance(channel, InputChannel):
766 # reset all solo/mute settings
767 for in_channel in self.channels:
768 self.monitor_channel.set_solo(in_channel.channel, False)
769 self.monitor_channel.set_muted(in_channel.channel, False)
770 self.monitor_channel.set_solo(channel.channel, True)
771 self.monitor_channel.prefader = True
772 self.monitor_channel.out_mute = False
773 else:
774 self.monitor_channel.prefader = False
775 self.monitor_channel.out_mute = False
777 if channel:
778 self.update_monitor(channel)
780 monitored_channel = property(get_monitored_channel, set_monitored_channel)
782 def update_monitor(self, channel):
783 if self._monitored_channel is not channel:
784 return
785 self.monitor_channel.volume = channel.channel.volume
786 self.monitor_channel.balance = channel.channel.balance
787 if isinstance(self.monitored_channel, OutputChannel):
788 # sync solo/muted channels
789 for input_channel in self.channels:
790 self.monitor_channel.set_solo(
791 input_channel.channel, channel.channel.is_solo(input_channel.channel)
793 self.monitor_channel.set_muted(
794 input_channel.channel, channel.channel.is_muted(input_channel.channel)
797 def get_input_channel_by_name(self, name):
798 for input_channel in self.channels:
799 if input_channel.channel.name == name:
800 return input_channel
801 return None
803 # ---------------------------------------------------------------------------------------------
804 # Mixer settings (de-)serialization and file handling
806 def save_to_xml(self, file):
807 log.debug("Saving to XML...")
808 b = XmlSerialization()
809 s = Serializator()
810 s.serialize(self, b)
811 b.save(file)
813 def load_from_xml(self, file, silence_errors=False, from_nsm=False):
814 log.debug("Loading from XML...")
815 self.unserialized_channels = []
816 b = XmlSerialization()
817 try:
818 b.load(file)
819 except: # noqa: E722
820 if silence_errors:
821 return
822 raise
823 self.on_channels_clear(None)
824 s = Serializator()
825 s.unserialize(self, b)
826 for channel in self.unserialized_channels:
827 if isinstance(channel, InputChannel):
828 if self._init_solo_channels and channel.channel_name in self._init_solo_channels:
829 channel.solo = True
830 self.add_channel_precreated(channel)
831 self._init_solo_channels = None
832 for channel in self.unserialized_channels:
833 if isinstance(channel, OutputChannel):
834 self.add_output_channel_precreated(channel)
835 del self.unserialized_channels
836 width, height = self.window.get_size()
837 if self.visible or not from_nsm:
838 self.window.show_all()
840 if self.output_channels:
841 self.output_channels[-1].volume_digits.select_region(0, 0)
842 self.output_channels[-1].slider.grab_focus()
843 elif self.channels:
844 self.channels[-1].volume_digits.select_region(0, 0)
845 self.channels[-1].volume_digits.grab_focus()
847 self.paned.set_position(self.paned_position / self.width * width)
848 self.window.resize(self.width, self.height)
850 def serialize(self, object_backend):
851 width, height = self.window.get_size()
852 object_backend.add_property("geometry", "%sx%s" % (width, height))
853 pos = self.paned.get_position()
854 object_backend.add_property("paned_position", "%s" % pos)
855 solo_channels = []
856 for input_channel in self.channels:
857 if input_channel.channel.solo:
858 solo_channels.append(input_channel)
859 if solo_channels:
860 object_backend.add_property(
861 "solo_channels", "|".join([x.channel.name for x in solo_channels])
863 object_backend.add_property("visible", "%s" % str(self.visible))
865 def unserialize_property(self, name, value):
866 if name == "geometry":
867 width, height = value.split("x")
868 self.width = int(width)
869 self.height = int(height)
870 return True
871 if name == "solo_channels":
872 self._init_solo_channels = value.split("|")
873 return True
874 if name == "visible":
875 self.visible = value == "True"
876 return True
877 if name == "paned_position":
878 self.paned_position = int(value)
879 return True
880 return False
882 def unserialize_child(self, name):
883 if name == InputChannel.serialization_name():
884 channel = InputChannel(self, "", True)
885 self.unserialized_channels.append(channel)
886 return channel
888 if name == OutputChannel.serialization_name():
889 channel = OutputChannel(self, "", True)
890 self.unserialized_channels.append(channel)
891 return channel
893 if name == gui.Factory.serialization_name():
894 return self.gui_factory
896 def serialization_get_childs(self):
897 """Get child objects that required and support serialization"""
898 childs = self.channels[:] + self.output_channels[:] + [self.gui_factory]
899 return childs
901 def serialization_name(self):
902 return "jack_mixer"
904 # ---------------------------------------------------------------------------------------------
905 # Main program loop
907 def main(self):
908 if not self.mixer:
909 return
911 if self.visible or self.nsm_client is None:
912 width, height = self.window.get_size()
913 self.window.show_all()
914 if hasattr(self, "paned_position"):
915 self.paned.set_position(self.paned_position / self.width * width)
917 signal.signal(signal.SIGUSR1, self.sighandler)
918 signal.signal(signal.SIGTERM, self.sighandler)
919 signal.signal(signal.SIGINT, self.sighandler)
920 signal.signal(signal.SIGHUP, signal.SIG_IGN)
922 Gtk.main()
925 def error_dialog(parent, msg, *args):
926 log.exception(msg, *args)
927 err = Gtk.MessageDialog(
928 parent=parent,
929 modal=True,
930 destroy_with_parent=True,
931 message_type=Gtk.MessageType.ERROR,
932 buttons=Gtk.ButtonsType.OK,
933 text=msg % args,
935 err.run()
936 err.destroy()
939 def main():
940 parser = ArgumentParser()
941 parser.add_argument(
942 "-c", "--config", metavar="FILE", help="load mixer project configuration from FILE"
944 parser.add_argument("-d", "--debug", action="store_true", help="enable debug logging messages")
945 parser.add_argument(
946 "client_name", metavar="NAME", nargs="?", default="jack_mixer", help="set JACK client name"
948 args = parser.parse_args()
950 logging.basicConfig(
951 level=logging.DEBUG if args.debug else logging.INFO, format="%(levelname)s: %(message)s"
954 try:
955 mixer = JackMixer(args.client_name)
956 except Exception as e:
957 error_dialog(None, "Mixer creation failed:\n\n%s", e)
958 sys.exit(1)
960 if not mixer.nsm_client and args.config:
961 try:
962 with open(args.config) as fp:
963 mixer.load_from_xml(fp)
964 except Exception as exc:
965 error_dialog(mixer.window, "Error loading settings file '%s': %s", args.config, exc)
966 else:
967 mixer.current_filename = args.config
969 mixer.window.set_default_size(
970 60 * (1 + len(mixer.channels) + len(mixer.output_channels)), 300
973 mixer.main()
975 mixer.cleanup()
978 if __name__ == "__main__":
979 main()