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-2021 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.
29 from argparse
import ArgumentParser
30 from urllib
.parse
import urlparse
34 gi
.require_version("Gtk", "3.0")
35 from gi
.repository
import Gtk
36 from gi
.repository
import GLib
38 # temporary change Python modules lookup path to look into installation
39 # directory ($prefix/share/jack_mixer/)
41 sys
.path
.insert(0, os
.path
.join(os
.path
.dirname(sys
.argv
[0]), "..", "share", "jack_mixer"))
47 from channel
import InputChannel
, NewInputChannelDialog
, NewOutputChannelDialog
, OutputChannel
48 from nsmclient
import NSMClient
49 from serialization_xml
import XmlSerialization
50 from serialization
import SerializedObject
, Serializator
51 from styling
import load_css_styles
52 from preferences
import PreferencesDialog
53 from version
import __version__
55 # restore Python modules lookup path
57 log
= logging
.getLogger("jack_mixer")
60 def add_number_suffix(s
):
62 return str(int(match
.group(0)) + 1)
64 new_s
= re
.sub(r
"(\d+)\s*$", inc
, s
)
71 class JackMixer(SerializedObject
):
73 # scales suitable as meter scales
79 scale
.IEC268Minimalistic(),
82 # scales suitable as volume slider scales
83 slider_scales
= [scale
.Linear30dB(), scale
.Linear70dB()]
85 def __init__(self
, client_name
="jack_mixer"):
87 self
.nsm_client
= None
88 # name of project file that is currently open
89 self
.current_filename
= None
90 self
.last_project_path
= None
91 self
._monitored
_channel
= None
92 self
._init
_solo
_channels
= None
94 if os
.environ
.get("NSM_URL"):
95 self
.nsm_client
= NSMClient(
96 prettyName
="jack_mixer",
97 saveCallback
=self
.nsm_save_cb
,
98 openOrNewCallback
=self
.nsm_open_cb
,
99 supportsSaveStatus
=False,
100 hideGUICallback
=self
.nsm_hide_cb
,
101 showGUICallback
=self
.nsm_show_cb
,
102 exitProgramCallback
=self
.nsm_exit_cb
,
103 loggingLevel
="error",
105 self
.nsm_client
.announceGuiVisibility(self
.visible
)
108 self
.create_mixer(client_name
, with_nsm
=False)
110 def create_mixer(self
, client_name
, with_nsm
=True):
111 self
.mixer
= jack_mixer_c
.Mixer(client_name
)
113 raise RuntimeError("Failed to create Mixer instance.")
115 self
.create_ui(with_nsm
)
116 self
.window
.set_title(client_name
)
118 self
.monitor_channel
= self
.mixer
.add_output_channel("Monitor", True, True)
120 GLib
.timeout_add(33, self
.read_meters
)
121 GLib
.timeout_add(50, self
.midi_events_check
)
124 GLib
.timeout_add(200, self
.nsm_react
)
127 log
.debug("Cleaning jack_mixer.")
131 for channel
in self
.channels
:
136 # ---------------------------------------------------------------------------------------------
137 # UI creation and (de-)initialization
139 def new_menu_item(self
, title
, callback
=None, accel
=None, enabled
=True):
140 menuitem
= Gtk
.MenuItem
.new_with_mnemonic(title
)
141 menuitem
.set_sensitive(enabled
)
143 menuitem
.connect("activate", callback
)
145 key
, mod
= Gtk
.accelerator_parse(accel
)
146 menuitem
.add_accelerator(
147 "activate", self
.menu_accelgroup
, key
, mod
, Gtk
.AccelFlags
.VISIBLE
151 def create_recent_file_menu(self
):
152 recentmenu
= Gtk
.MenuItem
.new_with_mnemonic('_Recent Projects')
154 self
.recentmanager
= Gtk
.RecentManager
.get_default()
155 recentchooser
= Gtk
.RecentChooserMenu
.new_for_manager(self
.recentmanager
)
156 recentchooser
.set_sort_type(Gtk
.RecentSortType
.MRU
)
157 recentchooser
.set_local_only(True)
158 recentchooser
.set_limit(10)
159 recentchooser
.set_show_icons(True)
160 recentchooser
.set_show_numbers(True)
161 recentchooser
.set_show_tips(True)
162 recentfilter
= Gtk
.RecentFilter()
163 recentfilter
.add_application("jack_mixer.py")
164 recentfilter
.add_application("jack_mixer")
165 recentchooser
.add_filter(recentfilter
)
166 recentchooser
.connect('item-activated', self
.on_recent_file_chosen
)
167 recentmenu
.set_submenu(recentchooser
)
170 def create_ui(self
, with_nsm
):
172 self
.output_channels
= []
174 self
.window
= Gtk
.Window(type=Gtk
.WindowType
.TOPLEVEL
)
175 self
.window
.set_icon_name("jack_mixer")
176 self
.gui_factory
= gui
.Factory(self
.window
, self
.meter_scales
, self
.slider_scales
)
177 self
.gui_factory
.connect("midi-behavior-mode-changed", self
.on_midi_behavior_mode_changed
)
178 self
.gui_factory
.emit_midi_behavior_mode()
180 self
.vbox_top
= Gtk
.VBox()
181 self
.window
.add(self
.vbox_top
)
183 self
.menu_accelgroup
= Gtk
.AccelGroup()
184 self
.window
.add_accel_group(self
.menu_accelgroup
)
186 self
.menubar
= Gtk
.MenuBar()
187 self
.vbox_top
.pack_start(self
.menubar
, False, True, 0)
189 mixer_menu_item
= Gtk
.MenuItem
.new_with_mnemonic("_Mixer")
190 self
.menubar
.append(mixer_menu_item
)
191 edit_menu_item
= Gtk
.MenuItem
.new_with_mnemonic("_Edit")
192 self
.menubar
.append(edit_menu_item
)
193 help_menu_item
= Gtk
.MenuItem
.new_with_mnemonic("_Help")
194 self
.menubar
.append(help_menu_item
)
198 self
.paned_position
= 210
199 self
.window
.set_default_size(self
.width
, self
.height
)
201 # Mixer (and File) menu
202 self
.mixer_menu
= Gtk
.Menu()
203 mixer_menu_item
.set_submenu(self
.mixer_menu
)
205 self
.mixer_menu
.append(
206 self
.new_menu_item("New _Input Channel", self
.on_add_input_channel
, "<Control>N")
208 self
.mixer_menu
.append(
210 "New Output _Channel", self
.on_add_output_channel
, "<Shift><Control>N"
214 self
.mixer_menu
.append(Gtk
.SeparatorMenuItem())
216 self
.mixer_menu
.append(self
.new_menu_item("_Open...", self
.on_open_cb
, "<Control>O"))
218 # Recent files sub-menu
219 self
.mixer_menu
.append(self
.create_recent_file_menu())
221 self
.mixer_menu
.append(self
.new_menu_item("_Save", self
.on_save_cb
, "<Control>S"))
224 self
.mixer_menu
.append(
225 self
.new_menu_item("Save _As...", self
.on_save_as_cb
, "<Shift><Control>S")
228 self
.mixer_menu
.append(Gtk
.SeparatorMenuItem())
230 self
.mixer_menu
.append(self
.new_menu_item("_Hide", self
.nsm_hide_cb
, "<Control>W"))
232 self
.mixer_menu
.append(self
.new_menu_item("_Quit", self
.on_quit_cb
, "<Control>Q"))
235 edit_menu
= Gtk
.Menu()
236 edit_menu_item
.set_submenu(edit_menu
)
238 self
.channel_edit_input_menu_item
= self
.new_menu_item(
239 "_Edit Input Channel", enabled
=False
241 edit_menu
.append(self
.channel_edit_input_menu_item
)
242 self
.channel_edit_input_menu
= Gtk
.Menu()
243 self
.channel_edit_input_menu_item
.set_submenu(self
.channel_edit_input_menu
)
245 self
.channel_edit_output_menu_item
= self
.new_menu_item(
246 "E_dit Output Channel", enabled
=False
248 edit_menu
.append(self
.channel_edit_output_menu_item
)
249 self
.channel_edit_output_menu
= Gtk
.Menu()
250 self
.channel_edit_output_menu_item
.set_submenu(self
.channel_edit_output_menu
)
252 self
.channel_remove_input_menu_item
= self
.new_menu_item(
253 "_Remove Input Channel", enabled
=False
255 edit_menu
.append(self
.channel_remove_input_menu_item
)
256 self
.channel_remove_input_menu
= Gtk
.Menu()
257 self
.channel_remove_input_menu_item
.set_submenu(self
.channel_remove_input_menu
)
259 self
.channel_remove_output_menu_item
= self
.new_menu_item(
260 "Re_move Output Channel", enabled
=False
262 edit_menu
.append(self
.channel_remove_output_menu_item
)
263 self
.channel_remove_output_menu
= Gtk
.Menu()
264 self
.channel_remove_output_menu_item
.set_submenu(self
.channel_remove_output_menu
)
266 edit_menu
.append(Gtk
.SeparatorMenuItem())
268 self
.new_menu_item("Shrink Channels", self
.on_shrink_channels_cb
, "<Control>minus")
271 self
.new_menu_item("Expand Channels", self
.on_expand_channels_cb
, "<Control>plus")
273 edit_menu
.append(Gtk
.SeparatorMenuItem())
275 edit_menu
.append(self
.new_menu_item("_Clear", self
.on_channels_clear
, "<Control>X"))
276 edit_menu
.append(Gtk
.SeparatorMenuItem())
278 self
.preferences_dialog
= None
279 edit_menu
.append(self
.new_menu_item("_Preferences", self
.on_preferences_cb
, "<Control>P"))
282 help_menu
= Gtk
.Menu()
283 help_menu_item
.set_submenu(help_menu
)
285 help_menu
.append(self
.new_menu_item("_About", self
.on_about
, "F1"))
288 self
.hbox_top
= Gtk
.HBox()
289 self
.vbox_top
.pack_start(self
.hbox_top
, True, True, 0)
291 self
.scrolled_window
= Gtk
.ScrolledWindow()
292 self
.scrolled_window
.set_policy(Gtk
.PolicyType
.AUTOMATIC
, Gtk
.PolicyType
.AUTOMATIC
)
294 self
.hbox_inputs
= Gtk
.Box()
295 self
.hbox_inputs
.set_spacing(0)
296 self
.hbox_inputs
.set_border_width(0)
297 self
.hbox_top
.set_spacing(0)
298 self
.hbox_top
.set_border_width(0)
299 self
.scrolled_window
.add(self
.hbox_inputs
)
300 self
.hbox_outputs
= Gtk
.Box()
301 self
.hbox_outputs
.set_spacing(0)
302 self
.hbox_outputs
.set_border_width(0)
303 self
.scrolled_output
= Gtk
.ScrolledWindow()
304 self
.scrolled_output
.set_policy(Gtk
.PolicyType
.AUTOMATIC
, Gtk
.PolicyType
.AUTOMATIC
)
305 self
.scrolled_output
.add(self
.hbox_outputs
)
306 self
.paned
= Gtk
.HPaned()
307 self
.paned
.set_wide_handle(True)
308 self
.hbox_top
.pack_start(self
.paned
, True, True, 0)
309 self
.paned
.pack1(self
.scrolled_window
, True, False)
310 self
.paned
.pack2(self
.scrolled_output
, True, False)
312 self
.window
.connect("destroy", Gtk
.main_quit
)
313 self
.window
.connect("delete-event", self
.on_delete_event
)
315 # ---------------------------------------------------------------------------------------------
318 def add_channel(self
, name
, stereo
, volume_cc
, balance_cc
, mute_cc
, solo_cc
, value
):
320 channel
= InputChannel(self
, name
, stereo
, value
)
321 self
.add_channel_precreated(channel
)
323 error_dialog(self
.window
, "Input channel creation failed.")
326 channel
.assign_midi_ccs(volume_cc
, balance_cc
, mute_cc
, solo_cc
)
329 def add_channel_precreated(self
, channel
):
332 self
.hbox_inputs
.pack_start(frame
, False, True, 0)
335 channel_edit_menu_item
= Gtk
.MenuItem(label
=channel
.channel_name
)
336 self
.channel_edit_input_menu
.append(channel_edit_menu_item
)
337 channel_edit_menu_item
.connect("activate", self
.on_edit_input_channel
, channel
)
338 self
.channel_edit_input_menu_item
.set_sensitive(True)
340 channel_remove_menu_item
= Gtk
.MenuItem(label
=channel
.channel_name
)
341 self
.channel_remove_input_menu
.append(channel_remove_menu_item
)
342 channel_remove_menu_item
.connect("activate", self
.on_remove_input_channel
, channel
)
343 self
.channel_remove_input_menu_item
.set_sensitive(True)
345 self
.channels
.append(channel
)
347 for outputchannel
in self
.output_channels
:
348 channel
.add_control_group(outputchannel
)
350 # create post fader output channel matching the input channel
351 channel
.post_fader_output_channel
= self
.mixer
.add_output_channel(
352 channel
.channel
.name
+ " Out", channel
.channel
.is_stereo
, True
354 channel
.post_fader_output_channel
.volume
= 0
355 channel
.post_fader_output_channel
.set_solo(channel
.channel
, True)
357 channel
.connect("input-channel-order-changed", self
.on_input_channel_order_changed
)
359 def add_output_channel(
360 self
, name
, stereo
, volume_cc
, balance_cc
, mute_cc
, display_solo_buttons
, color
, value
363 channel
= OutputChannel(self
, name
, stereo
, value
)
364 channel
.display_solo_buttons
= display_solo_buttons
365 channel
.color
= color
366 self
.add_output_channel_precreated(channel
)
368 error_dialog(self
.window
, "Output channel creation failed")
371 channel
.assign_midi_ccs(volume_cc
, balance_cc
, mute_cc
)
374 def add_output_channel_precreated(self
, channel
):
377 self
.hbox_outputs
.pack_end(frame
, False, True, 0)
378 self
.hbox_outputs
.reorder_child(frame
, 0)
381 channel_edit_menu_item
= Gtk
.MenuItem(label
=channel
.channel_name
)
382 self
.channel_edit_output_menu
.append(channel_edit_menu_item
)
383 channel_edit_menu_item
.connect("activate", self
.on_edit_output_channel
, channel
)
384 self
.channel_edit_output_menu_item
.set_sensitive(True)
386 channel_remove_menu_item
= Gtk
.MenuItem(label
=channel
.channel_name
)
387 self
.channel_remove_output_menu
.append(channel_remove_menu_item
)
388 channel_remove_menu_item
.connect("activate", self
.on_remove_output_channel
, channel
)
389 self
.channel_remove_output_menu_item
.set_sensitive(True)
391 self
.output_channels
.append(channel
)
392 channel
.connect("output-channel-order-changed", self
.on_output_channel_order_changed
)
394 # ---------------------------------------------------------------------------------------------
395 # Signal/event handlers
397 # ---------------------------------------------------------------------------------------------
401 self
.nsm_client
.reactToMessage()
404 def nsm_hide_cb(self
, *args
):
407 self
.nsm_client
.announceGuiVisibility(False)
409 def nsm_show_cb(self
):
410 width
, height
= self
.window
.get_size()
411 self
.window
.show_all()
412 self
.paned
.set_position(self
.paned_position
/ self
.width
* width
)
415 self
.nsm_client
.announceGuiVisibility(True)
417 def nsm_open_cb(self
, path
, session_name
, client_name
):
418 self
.create_mixer(client_name
, with_nsm
=True)
419 self
.current_filename
= path
+ ".xml"
420 if os
.path
.isfile(self
.current_filename
):
422 with
open(self
.current_filename
, "r") as fp
:
423 self
.load_from_xml(fp
, from_nsm
=True)
424 except Exception as exc
:
425 # Re-raise with more meaningful error message
427 "Error loading project file '{}': {}".format(self
.current_filename
, exc
)
430 def nsm_save_cb(self
, path
, session_name
, client_name
):
431 self
.current_filename
= path
+ ".xml"
432 f
= open(self
.current_filename
, "w")
436 def nsm_exit_cb(self
, path
, session_name
, client_name
):
439 # ---------------------------------------------------------------------------------------------
442 def sighandler(self
, signum
, frame
):
443 log
.debug("Signal %d received.", signum
)
444 if signum
== signal
.SIGUSR1
:
445 GLib
.timeout_add(0, self
.on_save_cb
)
446 elif signum
== signal
.SIGINT
or signal
== signal
.SIGTERM
:
447 GLib
.timeout_add(0, self
.on_quit_cb
)
449 log
.warning("Unknown signal %d received.", signum
)
451 # ---------------------------------------------------------------------------------------------
454 def on_about(self
, *args
):
455 about
= Gtk
.AboutDialog()
456 about
.set_name("jack_mixer")
457 about
.set_program_name("jack_mixer")
459 "Copyright © 2006-2021\n"
461 "Frédéric Péters, Arnout Engelen,\n"
462 "Daniel Sheeler, Christopher Arndt"
466 jack_mixer is free software; you can redistribute it and/or modify it
467 under the terms of the GNU General Public License as published by the
468 Free Software Foundation; either version 2 of the License, or (at your
469 option) any later version.
471 jack_mixer is distributed in the hope that it will be useful, but
472 WITHOUT ANY WARRANTY; without even the implied warranty of
473 MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
474 General Public License for more details.
476 You should have received a copy of the GNU General Public License along
477 with jack_mixer; if not, write to the Free Software Foundation, Inc., 51
478 Franklin Street, Fifth Floor, Boston, MA 02110-130159 USA"""
482 "Nedko Arnaudov <nedko@arnaudov.name>",
483 "Christopher Arndt <chris@chrisarndt.de>",
484 "Arnout Engelen <arnouten@bzzt.net>",
485 "John Hedges <john@drystone.co.uk>",
486 "Olivier Humbert <trebmuh@tuxfamily.org>",
487 "Sarah Mischke <sarah@spooky-online.de>",
488 "Frédéric Péters <fpeters@0d.be>",
489 "Daniel Sheeler <dsheeler@pobox.com>",
490 "Athanasios Silis <athanasios.silis@gmail.com>",
493 about
.set_logo_icon_name("jack_mixer")
494 about
.set_version(__version__
)
495 about
.set_website("https://rdio.space/jackmixer/")
499 def on_delete_event(self
, widget
, event
):
504 return self
.on_quit_cb(on_delete
=True)
506 def add_file_filters(self
, dialog
):
507 filter_xml
= Gtk
.FileFilter()
508 filter_xml
.set_name("XML files")
509 filter_xml
.add_mime_type("text/xml")
510 dialog
.add_filter(filter_xml
)
511 filter_all
= Gtk
.FileFilter()
512 filter_all
.set_name("All files")
513 filter_all
.add_pattern("*")
514 dialog
.add_filter(filter_all
)
516 def _open_project(self
, filename
):
518 with
open(filename
, "r") as fp
:
519 self
.load_from_xml(fp
)
520 except Exception as exc
:
521 error_dialog(self
.window
, "Error loading project file '%s': %s", filename
, exc
)
523 self
.current_filename
= filename
526 def on_open_cb(self
, *args
):
527 dlg
= Gtk
.FileChooserDialog(
528 title
="Open project", parent
=self
.window
, action
=Gtk
.FileChooserAction
.OPEN
531 Gtk
.STOCK_CANCEL
, Gtk
.ResponseType
.CANCEL
, Gtk
.STOCK_OPEN
, Gtk
.ResponseType
.OK
533 dlg
.set_default_response(Gtk
.ResponseType
.OK
)
535 default_project_path
= self
.gui_factory
.get_default_project_path()
537 if self
.current_filename
:
538 dlg
.set_current_folder(os
.path
.dirname(self
.current_filename
))
540 dlg
.set_current_folder(self
.last_project_path
or default_project_path
or os
.getcwd())
542 if default_project_path
:
543 dlg
.add_shortcut_folder(default_project_path
)
545 self
.add_file_filters(dlg
)
547 if dlg
.run() == Gtk
.ResponseType
.OK
:
548 filename
= dlg
.get_filename()
549 if self
._open
_project
(filename
):
550 self
.recentmanager
.add_item("file://" + os
.path
.abspath(filename
))
554 def on_recent_file_chosen(self
, recentchooser
):
555 item
= recentchooser
.get_current_item()
557 if item
and item
.exists():
558 log
.debug("Recent file menu entry selected: %s", item
.get_display_name())
560 if not self
._open
_project
(urlparse(uri
).path
):
561 self
.recentmanager
.remove_item(uri
)
563 def _save_project(self
, filename
):
564 with
open(filename
, "w") as fp
:
567 def on_save_cb(self
, *args
):
568 if not self
.current_filename
:
569 return self
.on_save_as_cb()
572 self
._save
_project
(self
.current_filename
)
573 except Exception as exc
:
574 error_dialog(self
.window
, "Error saving project file '%s': %s",
575 self
.current_filename
, exc
)
577 def on_save_as_cb(self
, *args
):
578 dlg
= Gtk
.FileChooserDialog(
579 title
="Save project", parent
=self
.window
, action
=Gtk
.FileChooserAction
.SAVE
582 Gtk
.STOCK_CANCEL
, Gtk
.ResponseType
.CANCEL
, Gtk
.STOCK_SAVE
, Gtk
.ResponseType
.OK
584 dlg
.set_default_response(Gtk
.ResponseType
.OK
)
585 dlg
.set_do_overwrite_confirmation(True)
587 default_project_path
= self
.gui_factory
.get_default_project_path()
589 if self
.current_filename
:
590 dlg
.set_filename(self
.current_filename
)
592 dlg
.set_current_folder(self
.last_project_path
or default_project_path
or os
.getcwd())
593 filename
= "{}-{}.xml".format(
594 getpass
.getuser(), datetime
.datetime
.now().strftime('%Y%m%d-%H%M'))
595 dlg
.set_current_name(filename
)
597 if default_project_path
:
598 dlg
.add_shortcut_folder(default_project_path
)
600 self
.add_file_filters(dlg
)
602 if dlg
.run() == Gtk
.ResponseType
.OK
:
603 save_path
= dlg
.get_filename()
604 save_dir
= os
.path
.dirname(save_path
)
605 if os
.path
.isdir(save_dir
):
606 self
.last_project_path
= save_dir
608 filename
= dlg
.get_filename()
610 self
._save
_project
(filename
)
611 except Exception as exc
:
612 error_dialog(self
.window
, "Error saving project file '%s': %s", filename
, exc
)
614 self
.current_filename
= filename
615 self
.recentmanager
.add_item("file://" + os
.path
.abspath(filename
))
619 def on_quit_cb(self
, *args
, on_delete
=False):
620 if not self
.nsm_client
and self
.gui_factory
.get_confirm_quit():
621 dlg
= Gtk
.MessageDialog(
623 message_type
=Gtk
.MessageType
.QUESTION
,
624 buttons
=Gtk
.ButtonsType
.NONE
,
626 dlg
.set_markup("<b>Quit application?</b>")
627 dlg
.format_secondary_markup(
628 "All jack_mixer ports will be closed and connections lost,"
629 "\nstopping all sound going through jack_mixer.\n\n"
633 Gtk
.STOCK_CANCEL
, Gtk
.ResponseType
.CANCEL
, Gtk
.STOCK_QUIT
, Gtk
.ResponseType
.OK
637 if response
!= Gtk
.ResponseType
.OK
:
642 def on_shrink_channels_cb(self
, widget
):
643 for channel
in self
.channels
+ self
.output_channels
:
646 def on_expand_channels_cb(self
, widget
):
647 for channel
in self
.channels
+ self
.output_channels
:
650 def on_midi_behavior_mode_changed(self
, gui_factory
, value
):
651 self
.mixer
.midi_behavior_mode
= value
653 def on_preferences_cb(self
, widget
):
654 if not self
.preferences_dialog
:
655 self
.preferences_dialog
= PreferencesDialog(self
)
656 self
.preferences_dialog
.show()
657 self
.preferences_dialog
.present()
659 def on_add_channel(self
, inout
="input", default_name
="Input"):
660 dialog
= getattr(self
, "_add_{}_dialog".format(inout
), None)
661 values
= getattr(self
, "_add_{}_values".format(inout
), {})
664 cls
= NewInputChannelDialog
if inout
== "input" else NewOutputChannelDialog
665 dialog
= cls(app
=self
)
666 setattr(self
, "_add_{}_dialog".format(inout
), dialog
)
669 ch
.channel_name
for ch
in (self
.channels
if inout
== "input" else self
.output_channels
)
671 values
.setdefault("name", default_name
)
673 if values
["name"] in names
:
674 values
["name"] = add_number_suffix(values
["name"])
678 dialog
.fill_ui(**values
)
679 dialog
.set_transient_for(self
.window
)
684 if ret
== Gtk
.ResponseType
.OK
:
685 result
= dialog
.get_result()
686 setattr(self
, "_add_{}_values".format(inout
), result
)
687 (self
.add_channel
if inout
== "input" else self
.add_output_channel
)(**result
)
688 if self
.visible
or self
.nsm_client
is None:
689 self
.window
.show_all()
691 def on_add_input_channel(self
, widget
):
692 return self
.on_add_channel("input", "Input")
694 def on_add_output_channel(self
, widget
):
695 return self
.on_add_channel("output", "Output")
697 def on_edit_input_channel(self
, widget
, channel
):
698 log
.debug('Editing input channel "%s".', channel
.channel_name
)
699 channel
.on_channel_properties()
701 def on_remove_input_channel(self
, widget
, channel
):
702 log
.debug('Removing input channel "%s".', channel
.channel_name
)
704 def remove_channel_edit_input_menuitem_by_label(widget
, label
):
705 if widget
.get_label() == label
:
706 self
.channel_edit_input_menu
.remove(widget
)
708 self
.channel_remove_input_menu
.remove(widget
)
709 self
.channel_edit_input_menu
.foreach(
710 remove_channel_edit_input_menuitem_by_label
, channel
.channel_name
713 if self
.monitored_channel
is channel
:
714 channel
.monitor_button
.set_active(False)
716 for i
in range(len(self
.channels
)):
717 if self
.channels
[i
] is channel
:
720 self
.hbox_inputs
.remove(channel
.get_parent())
723 if not self
.channels
:
724 self
.channel_edit_input_menu_item
.set_sensitive(False)
725 self
.channel_remove_input_menu_item
.set_sensitive(False)
727 def on_edit_output_channel(self
, widget
, channel
):
728 log
.debug('Editing output channel "%s".', channel
.channel_name
)
729 channel
.on_channel_properties()
731 def on_remove_output_channel(self
, widget
, channel
):
732 log
.debug('Removing output channel "%s".', channel
.channel_name
)
734 def remove_channel_edit_output_menuitem_by_label(widget
, label
):
735 if widget
.get_label() == label
:
736 self
.channel_edit_output_menu
.remove(widget
)
738 self
.channel_remove_output_menu
.remove(widget
)
739 self
.channel_edit_output_menu
.foreach(
740 remove_channel_edit_output_menuitem_by_label
, channel
.channel_name
743 if self
.monitored_channel
is channel
:
744 channel
.monitor_button
.set_active(False)
746 for i
in range(len(self
.channels
)):
747 if self
.output_channels
[i
] is channel
:
749 del self
.output_channels
[i
]
750 self
.hbox_outputs
.remove(channel
.get_parent())
753 if not self
.output_channels
:
754 self
.channel_edit_output_menu_item
.set_sensitive(False)
755 self
.channel_remove_output_menu_item
.set_sensitive(False)
757 def on_channel_rename(self
, oldname
, newname
):
758 def rename_channels(container
, parameters
):
759 if container
.get_label() == parameters
["oldname"]:
760 container
.set_label(parameters
["newname"])
762 rename_parameters
= {"oldname": oldname
, "newname": newname
}
763 self
.channel_edit_input_menu
.foreach(rename_channels
, rename_parameters
)
764 self
.channel_edit_output_menu
.foreach(rename_channels
, rename_parameters
)
765 self
.channel_remove_input_menu
.foreach(rename_channels
, rename_parameters
)
766 self
.channel_remove_output_menu
.foreach(rename_channels
, rename_parameters
)
767 log
.debug('Renaming channel from "%s" to "%s".', oldname
, newname
)
769 def on_input_channel_order_changed(self
, widget
, source_name
, dest_name
):
770 self
.channels
.clear()
772 channel_box
= self
.hbox_inputs
773 frames
= channel_box
.get_children()
777 if source_name
== c
._channel
_name
:
783 if dest_name
== c
._channel
_name
:
784 pos
= frames
.index(f
)
785 channel_box
.reorder_child(source_frame
, pos
)
788 for frame
in self
.hbox_inputs
.get_children():
789 c
= frame
.get_child()
790 self
.channels
.append(c
)
792 def on_output_channel_order_changed(self
, widget
, source_name
, dest_name
):
793 self
.output_channels
.clear()
794 channel_box
= self
.hbox_outputs
796 frames
= channel_box
.get_children()
800 if source_name
== c
._channel
_name
:
806 if dest_name
== c
._channel
_name
:
807 pos
= len(frames
) - 1 - frames
.index(f
)
808 channel_box
.reorder_child(source_frame
, pos
)
811 for frame
in self
.hbox_outputs
.get_children():
812 c
= frame
.get_child()
813 self
.output_channels
.append(c
)
815 def on_channels_clear(self
, widget
):
816 dlg
= Gtk
.MessageDialog(
819 message_type
=Gtk
.MessageType
.WARNING
,
820 text
="Are you sure you want to clear all channels?",
821 buttons
=Gtk
.ButtonsType
.OK_CANCEL
,
824 if not widget
or dlg
.run() == Gtk
.ResponseType
.OK
:
825 for channel
in self
.output_channels
:
827 self
.hbox_outputs
.remove(channel
.get_parent())
828 for channel
in self
.channels
:
830 self
.hbox_inputs
.remove(channel
.get_parent())
832 self
.output_channels
= []
833 self
.channel_edit_input_menu
= Gtk
.Menu()
834 self
.channel_edit_input_menu_item
.set_submenu(self
.channel_edit_input_menu
)
835 self
.channel_edit_input_menu_item
.set_sensitive(False)
836 self
.channel_remove_input_menu
= Gtk
.Menu()
837 self
.channel_remove_input_menu_item
.set_submenu(self
.channel_remove_input_menu
)
838 self
.channel_remove_input_menu_item
.set_sensitive(False)
839 self
.channel_edit_output_menu
= Gtk
.Menu()
840 self
.channel_edit_output_menu_item
.set_submenu(self
.channel_edit_output_menu
)
841 self
.channel_edit_output_menu_item
.set_sensitive(False)
842 self
.channel_remove_output_menu
= Gtk
.Menu()
843 self
.channel_remove_output_menu_item
.set_submenu(self
.channel_remove_output_menu
)
844 self
.channel_remove_output_menu_item
.set_sensitive(False)
846 # Force save-as dialog on next save
847 self
.current_filename
= None
851 def read_meters(self
):
852 for channel
in self
.channels
:
854 for channel
in self
.output_channels
:
858 def midi_events_check(self
):
859 for channel
in self
.channels
+ self
.output_channels
:
860 channel
.midi_events_check()
863 def get_monitored_channel(self
):
864 return self
._monitored
_channel
866 def set_monitored_channel(self
, channel
):
867 if channel
== self
._monitored
_channel
:
869 self
._monitored
_channel
= channel
871 self
.monitor_channel
.out_mute
= True
872 elif isinstance(channel
, InputChannel
):
873 # reset all solo/mute settings
874 for in_channel
in self
.channels
:
875 self
.monitor_channel
.set_solo(in_channel
.channel
, False)
876 self
.monitor_channel
.set_muted(in_channel
.channel
, False)
877 self
.monitor_channel
.set_solo(channel
.channel
, True)
878 self
.monitor_channel
.prefader
= True
879 self
.monitor_channel
.out_mute
= False
881 self
.monitor_channel
.prefader
= False
882 self
.monitor_channel
.out_mute
= False
885 self
.update_monitor(channel
)
887 monitored_channel
= property(get_monitored_channel
, set_monitored_channel
)
889 def update_monitor(self
, channel
):
890 if self
._monitored
_channel
is not channel
:
892 self
.monitor_channel
.volume
= channel
.channel
.volume
893 self
.monitor_channel
.balance
= channel
.channel
.balance
894 if isinstance(self
.monitored_channel
, OutputChannel
):
895 # sync solo/muted channels
896 for input_channel
in self
.channels
:
897 self
.monitor_channel
.set_solo(
898 input_channel
.channel
, channel
.channel
.is_solo(input_channel
.channel
)
900 self
.monitor_channel
.set_muted(
901 input_channel
.channel
, channel
.channel
.is_muted(input_channel
.channel
)
904 def get_input_channel_by_name(self
, name
):
905 for input_channel
in self
.channels
:
906 if input_channel
.channel
.name
== name
:
910 # ---------------------------------------------------------------------------------------------
911 # Mixer project (de-)serialization and file handling
913 def save_to_xml(self
, file):
914 log
.debug("Saving to XML...")
915 b
= XmlSerialization()
920 def load_from_xml(self
, file, silence_errors
=False, from_nsm
=False):
921 log
.debug("Loading from XML...")
922 self
.unserialized_channels
= []
923 b
= XmlSerialization()
925 b
.load(file, self
.serialization_name())
931 self
.on_channels_clear(None)
933 s
.unserialize(self
, b
)
934 for channel
in self
.unserialized_channels
:
935 if isinstance(channel
, InputChannel
):
936 if self
._init
_solo
_channels
and channel
.channel_name
in self
._init
_solo
_channels
:
938 self
.add_channel_precreated(channel
)
939 self
._init
_solo
_channels
= None
940 for channel
in self
.unserialized_channels
:
941 if isinstance(channel
, OutputChannel
):
942 self
.add_output_channel_precreated(channel
)
943 del self
.unserialized_channels
944 width
, height
= self
.window
.get_size()
945 if self
.visible
or not from_nsm
:
946 self
.window
.show_all()
948 if self
.output_channels
:
949 self
.output_channels
[-1].volume_digits
.select_region(0, 0)
950 self
.output_channels
[-1].slider
.grab_focus()
952 self
.channels
[-1].volume_digits
.select_region(0, 0)
953 self
.channels
[-1].volume_digits
.grab_focus()
955 self
.paned
.set_position(self
.paned_position
/ self
.width
* width
)
956 self
.window
.resize(self
.width
, self
.height
)
958 def serialize(self
, object_backend
):
959 width
, height
= self
.window
.get_size()
960 object_backend
.add_property("geometry", "%sx%s" % (width
, height
))
961 pos
= self
.paned
.get_position()
962 object_backend
.add_property("paned_position", "%s" % pos
)
964 for input_channel
in self
.channels
:
965 if input_channel
.channel
.solo
:
966 solo_channels
.append(input_channel
)
968 object_backend
.add_property(
969 "solo_channels", "|".join([x
.channel
.name
for x
in solo_channels
])
971 object_backend
.add_property("visible", "%s" % str(self
.visible
))
973 def unserialize_property(self
, name
, value
):
974 if name
== "geometry":
975 width
, height
= value
.split("x")
976 self
.width
= int(width
)
977 self
.height
= int(height
)
979 if name
== "solo_channels":
980 self
._init
_solo
_channels
= value
.split("|")
982 if name
== "visible":
983 self
.visible
= value
== "True"
985 if name
== "paned_position":
986 self
.paned_position
= int(value
)
990 def unserialize_child(self
, name
):
991 if name
== InputChannel
.serialization_name():
992 channel
= InputChannel(self
, "", True)
993 self
.unserialized_channels
.append(channel
)
996 if name
== OutputChannel
.serialization_name():
997 channel
= OutputChannel(self
, "", True)
998 self
.unserialized_channels
.append(channel
)
1001 if name
== gui
.Factory
.serialization_name():
1002 return self
.gui_factory
1004 def serialization_get_childs(self
):
1005 """Get child objects that required and support serialization"""
1006 childs
= self
.channels
[:] + self
.output_channels
[:] + [self
.gui_factory
]
1009 def serialization_name(self
):
1012 # ---------------------------------------------------------------------------------------------
1019 if self
.visible
or self
.nsm_client
is None:
1020 width
, height
= self
.window
.get_size()
1021 self
.window
.show_all()
1022 if hasattr(self
, "paned_position"):
1023 self
.paned
.set_position(self
.paned_position
/ self
.width
* width
)
1025 signal
.signal(signal
.SIGUSR1
, self
.sighandler
)
1026 signal
.signal(signal
.SIGTERM
, self
.sighandler
)
1027 signal
.signal(signal
.SIGINT
, self
.sighandler
)
1028 signal
.signal(signal
.SIGHUP
, signal
.SIG_IGN
)
1033 def error_dialog(parent
, msg
, *args
):
1034 log
.exception(msg
, *args
)
1035 err
= Gtk
.MessageDialog(
1038 destroy_with_parent
=True,
1039 message_type
=Gtk
.MessageType
.ERROR
,
1040 buttons
=Gtk
.ButtonsType
.OK
,
1048 parser
= ArgumentParser()
1049 parser
.add_argument(
1050 "-c", "--config", metavar
="FILE", help="load mixer project configuration from FILE"
1052 parser
.add_argument("-d", "--debug", action
="store_true", help="enable debug logging messages")
1053 parser
.add_argument(
1054 "client_name", metavar
="NAME", nargs
="?", default
="jack_mixer", help="set JACK client name"
1056 args
= parser
.parse_args()
1058 logging
.basicConfig(
1059 level
=logging
.DEBUG
if args
.debug
else logging
.INFO
, format
="%(levelname)s: %(message)s"
1063 mixer
= JackMixer(args
.client_name
)
1064 except Exception as e
:
1065 error_dialog(None, "Mixer creation failed:\n\n%s", e
)
1068 if not mixer
.nsm_client
and args
.config
:
1070 with
open(args
.config
) as fp
:
1071 mixer
.load_from_xml(fp
)
1072 except Exception as exc
:
1073 error_dialog(mixer
.window
, "Error loading project file '%s': %s", args
.config
, exc
)
1075 mixer
.current_filename
= args
.config
1077 mixer
.window
.set_default_size(
1078 60 * (1 + len(mixer
.channels
) + len(mixer
.output_channels
)), 300
1086 if __name__
== "__main__":