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 def filter_func(item
):
153 return item
.mime_type
in ("text/xml", "application/xml") and (
154 "jack_mixer.py" in item
.applications
or "jack_mixer" in item
.applications
157 filter_flags
= Gtk
.RecentFilterFlags
.MIME_TYPE | Gtk
.RecentFilterFlags
.APPLICATION
158 recentfilter
= Gtk
.RecentFilter()
159 recentfilter
.set_name("jack_mixer XML files")
160 recentfilter
.add_custom(filter_flags
, filter_func
)
162 recentchooser
= Gtk
.RecentChooserMenu
.new_for_manager(self
.recentmanager
)
163 recentchooser
.set_sort_type(Gtk
.RecentSortType
.MRU
)
164 recentchooser
.set_local_only(True)
165 recentchooser
.set_limit(10)
166 recentchooser
.set_show_icons(True)
167 recentchooser
.set_show_numbers(True)
168 recentchooser
.set_show_tips(True)
169 recentchooser
.add_filter(recentfilter
)
170 recentchooser
.connect("item-activated", self
.on_recent_file_chosen
)
172 recentmenu
= Gtk
.MenuItem
.new_with_mnemonic("_Recent Projects")
173 recentmenu
.set_submenu(recentchooser
)
176 def create_ui(self
, with_nsm
):
178 self
.output_channels
= []
184 self
.paned_position
= 210
185 self
.window
= Gtk
.Window(type=Gtk
.WindowType
.TOPLEVEL
)
186 self
.window
.set_icon_name("jack_mixer")
187 self
.window
.set_default_size(self
.width
, self
.height
)
189 self
.gui_factory
= gui
.Factory(self
.window
, self
.meter_scales
, self
.slider_scales
)
190 self
.gui_factory
.connect("midi-behavior-mode-changed", self
.on_midi_behavior_mode_changed
)
191 self
.gui_factory
.emit_midi_behavior_mode()
193 # Recent files manager
194 self
.recentmanager
= Gtk
.RecentManager
.get_default()
196 self
.vbox_top
= Gtk
.VBox()
197 self
.window
.add(self
.vbox_top
)
199 self
.menu_accelgroup
= Gtk
.AccelGroup()
200 self
.window
.add_accel_group(self
.menu_accelgroup
)
203 self
.menubar
= Gtk
.MenuBar()
204 self
.vbox_top
.pack_start(self
.menubar
, False, True, 0)
206 mixer_menu_item
= Gtk
.MenuItem
.new_with_mnemonic("_Mixer")
207 self
.menubar
.append(mixer_menu_item
)
208 edit_menu_item
= Gtk
.MenuItem
.new_with_mnemonic("_Edit")
209 self
.menubar
.append(edit_menu_item
)
210 help_menu_item
= Gtk
.MenuItem
.new_with_mnemonic("_Help")
211 self
.menubar
.append(help_menu_item
)
213 # Mixer (and File) menu
214 self
.mixer_menu
= Gtk
.Menu()
215 mixer_menu_item
.set_submenu(self
.mixer_menu
)
217 self
.mixer_menu
.append(
218 self
.new_menu_item("New _Input Channel", self
.on_add_input_channel
, "<Control>N")
220 self
.mixer_menu
.append(
222 "New Output _Channel", self
.on_add_output_channel
, "<Shift><Control>N"
226 self
.mixer_menu
.append(Gtk
.SeparatorMenuItem())
228 self
.mixer_menu
.append(self
.new_menu_item("_Open...", self
.on_open_cb
, "<Control>O"))
230 # Recent files sub-menu
231 self
.mixer_menu
.append(self
.create_recent_file_menu())
233 self
.mixer_menu
.append(self
.new_menu_item("_Save", self
.on_save_cb
, "<Control>S"))
236 self
.mixer_menu
.append(
237 self
.new_menu_item("Save _As...", self
.on_save_as_cb
, "<Shift><Control>S")
240 self
.mixer_menu
.append(Gtk
.SeparatorMenuItem())
242 self
.mixer_menu
.append(self
.new_menu_item("_Hide", self
.nsm_hide_cb
, "<Control>W"))
244 self
.mixer_menu
.append(self
.new_menu_item("_Quit", self
.on_quit_cb
, "<Control>Q"))
247 edit_menu
= Gtk
.Menu()
248 edit_menu_item
.set_submenu(edit_menu
)
250 self
.channel_edit_input_menu_item
= self
.new_menu_item(
251 "_Edit Input Channel", enabled
=False
253 edit_menu
.append(self
.channel_edit_input_menu_item
)
254 self
.channel_edit_input_menu
= Gtk
.Menu()
255 self
.channel_edit_input_menu_item
.set_submenu(self
.channel_edit_input_menu
)
257 self
.channel_edit_output_menu_item
= self
.new_menu_item(
258 "E_dit Output Channel", enabled
=False
260 edit_menu
.append(self
.channel_edit_output_menu_item
)
261 self
.channel_edit_output_menu
= Gtk
.Menu()
262 self
.channel_edit_output_menu_item
.set_submenu(self
.channel_edit_output_menu
)
264 self
.channel_remove_input_menu_item
= self
.new_menu_item(
265 "_Remove Input Channel", enabled
=False
267 edit_menu
.append(self
.channel_remove_input_menu_item
)
268 self
.channel_remove_input_menu
= Gtk
.Menu()
269 self
.channel_remove_input_menu_item
.set_submenu(self
.channel_remove_input_menu
)
271 self
.channel_remove_output_menu_item
= self
.new_menu_item(
272 "Re_move Output Channel", enabled
=False
274 edit_menu
.append(self
.channel_remove_output_menu_item
)
275 self
.channel_remove_output_menu
= Gtk
.Menu()
276 self
.channel_remove_output_menu_item
.set_submenu(self
.channel_remove_output_menu
)
278 edit_menu
.append(Gtk
.SeparatorMenuItem())
280 self
.new_menu_item("Shrink Channels", self
.on_shrink_channels_cb
, "<Control>minus")
283 self
.new_menu_item("Expand Channels", self
.on_expand_channels_cb
, "<Control>plus")
285 edit_menu
.append(Gtk
.SeparatorMenuItem())
287 edit_menu
.append(self
.new_menu_item("_Clear", self
.on_channels_clear
, "<Control>X"))
288 edit_menu
.append(Gtk
.SeparatorMenuItem())
290 self
.preferences_dialog
= None
291 edit_menu
.append(self
.new_menu_item("_Preferences", self
.on_preferences_cb
, "<Control>P"))
294 help_menu
= Gtk
.Menu()
295 help_menu_item
.set_submenu(help_menu
)
297 help_menu
.append(self
.new_menu_item("_About", self
.on_about
, "F1"))
300 self
.hbox_top
= Gtk
.HBox()
301 self
.vbox_top
.pack_start(self
.hbox_top
, True, True, 0)
303 self
.scrolled_window
= Gtk
.ScrolledWindow()
304 self
.scrolled_window
.set_policy(Gtk
.PolicyType
.AUTOMATIC
, Gtk
.PolicyType
.AUTOMATIC
)
306 self
.hbox_inputs
= Gtk
.Box()
307 self
.hbox_inputs
.set_spacing(0)
308 self
.hbox_inputs
.set_border_width(0)
309 self
.hbox_top
.set_spacing(0)
310 self
.hbox_top
.set_border_width(0)
311 self
.scrolled_window
.add(self
.hbox_inputs
)
312 self
.hbox_outputs
= Gtk
.Box()
313 self
.hbox_outputs
.set_spacing(0)
314 self
.hbox_outputs
.set_border_width(0)
315 self
.scrolled_output
= Gtk
.ScrolledWindow()
316 self
.scrolled_output
.set_policy(Gtk
.PolicyType
.AUTOMATIC
, Gtk
.PolicyType
.AUTOMATIC
)
317 self
.scrolled_output
.add(self
.hbox_outputs
)
318 self
.paned
= Gtk
.HPaned()
319 self
.paned
.set_wide_handle(True)
320 self
.hbox_top
.pack_start(self
.paned
, True, True, 0)
321 self
.paned
.pack1(self
.scrolled_window
, True, False)
322 self
.paned
.pack2(self
.scrolled_output
, True, False)
324 self
.window
.connect("destroy", Gtk
.main_quit
)
325 self
.window
.connect("delete-event", self
.on_delete_event
)
327 # ---------------------------------------------------------------------------------------------
330 def add_channel(self
, name
, stereo
, volume_cc
, balance_cc
, mute_cc
, solo_cc
, value
):
332 channel
= InputChannel(self
, name
, stereo
, value
)
333 self
.add_channel_precreated(channel
)
335 error_dialog(self
.window
, "Input channel creation failed.")
338 channel
.assign_midi_ccs(volume_cc
, balance_cc
, mute_cc
, solo_cc
)
341 def add_channel_precreated(self
, channel
):
344 self
.hbox_inputs
.pack_start(frame
, False, True, 0)
347 channel_edit_menu_item
= Gtk
.MenuItem(label
=channel
.channel_name
)
348 self
.channel_edit_input_menu
.append(channel_edit_menu_item
)
349 channel_edit_menu_item
.connect("activate", self
.on_edit_input_channel
, channel
)
350 self
.channel_edit_input_menu_item
.set_sensitive(True)
352 channel_remove_menu_item
= Gtk
.MenuItem(label
=channel
.channel_name
)
353 self
.channel_remove_input_menu
.append(channel_remove_menu_item
)
354 channel_remove_menu_item
.connect("activate", self
.on_remove_input_channel
, channel
)
355 self
.channel_remove_input_menu_item
.set_sensitive(True)
357 self
.channels
.append(channel
)
359 for outputchannel
in self
.output_channels
:
360 channel
.add_control_group(outputchannel
)
362 # create post fader output channel matching the input channel
363 channel
.post_fader_output_channel
= self
.mixer
.add_output_channel(
364 channel
.channel
.name
+ " Out", channel
.channel
.is_stereo
, True
366 channel
.post_fader_output_channel
.volume
= 0
367 channel
.post_fader_output_channel
.set_solo(channel
.channel
, True)
369 channel
.connect("input-channel-order-changed", self
.on_input_channel_order_changed
)
371 def add_output_channel(
372 self
, name
, stereo
, volume_cc
, balance_cc
, mute_cc
, display_solo_buttons
, color
, value
375 channel
= OutputChannel(self
, name
, stereo
, value
)
376 channel
.display_solo_buttons
= display_solo_buttons
377 channel
.color
= color
378 self
.add_output_channel_precreated(channel
)
380 error_dialog(self
.window
, "Output channel creation failed")
383 channel
.assign_midi_ccs(volume_cc
, balance_cc
, mute_cc
)
386 def add_output_channel_precreated(self
, channel
):
389 self
.hbox_outputs
.pack_end(frame
, False, True, 0)
390 self
.hbox_outputs
.reorder_child(frame
, 0)
393 channel_edit_menu_item
= Gtk
.MenuItem(label
=channel
.channel_name
)
394 self
.channel_edit_output_menu
.append(channel_edit_menu_item
)
395 channel_edit_menu_item
.connect("activate", self
.on_edit_output_channel
, channel
)
396 self
.channel_edit_output_menu_item
.set_sensitive(True)
398 channel_remove_menu_item
= Gtk
.MenuItem(label
=channel
.channel_name
)
399 self
.channel_remove_output_menu
.append(channel_remove_menu_item
)
400 channel_remove_menu_item
.connect("activate", self
.on_remove_output_channel
, channel
)
401 self
.channel_remove_output_menu_item
.set_sensitive(True)
403 self
.output_channels
.append(channel
)
404 channel
.connect("output-channel-order-changed", self
.on_output_channel_order_changed
)
406 # ---------------------------------------------------------------------------------------------
407 # Signal/event handlers
409 # ---------------------------------------------------------------------------------------------
413 self
.nsm_client
.reactToMessage()
416 def nsm_hide_cb(self
, *args
):
419 self
.nsm_client
.announceGuiVisibility(False)
421 def nsm_show_cb(self
):
422 width
, height
= self
.window
.get_size()
423 self
.window
.show_all()
424 self
.paned
.set_position(self
.paned_position
/ self
.width
* width
)
427 self
.nsm_client
.announceGuiVisibility(True)
429 def nsm_open_cb(self
, path
, session_name
, client_name
):
430 self
.create_mixer(client_name
, with_nsm
=True)
431 self
.current_filename
= path
+ ".xml"
432 if os
.path
.isfile(self
.current_filename
):
434 with
open(self
.current_filename
, "r") as fp
:
435 self
.load_from_xml(fp
, from_nsm
=True)
436 except Exception as exc
:
437 # Re-raise with more meaningful error message
439 "Error loading project file '{}': {}".format(self
.current_filename
, exc
)
442 def nsm_save_cb(self
, path
, session_name
, client_name
):
443 self
.current_filename
= path
+ ".xml"
444 f
= open(self
.current_filename
, "w")
448 def nsm_exit_cb(self
, path
, session_name
, client_name
):
451 # ---------------------------------------------------------------------------------------------
454 def sighandler(self
, signum
, frame
):
455 log
.debug("Signal %d received.", signum
)
456 if signum
== signal
.SIGUSR1
:
457 GLib
.timeout_add(0, self
.on_save_cb
)
458 elif signum
== signal
.SIGINT
or signum
== signal
.SIGTERM
:
459 GLib
.timeout_add(0, self
.on_quit_cb
)
461 log
.warning("Unknown signal %d received.", signum
)
463 # ---------------------------------------------------------------------------------------------
466 def on_about(self
, *args
):
467 about
= Gtk
.AboutDialog()
468 about
.set_name("jack_mixer")
469 about
.set_program_name("jack_mixer")
471 "Copyright © 2006-2021\n"
473 "Frédéric Péters, Arnout Engelen,\n"
474 "Daniel Sheeler, Christopher Arndt"
478 jack_mixer is free software; you can redistribute it and/or modify it
479 under the terms of the GNU General Public License as published by the
480 Free Software Foundation; either version 2 of the License, or (at your
481 option) any later version.
483 jack_mixer is distributed in the hope that it will be useful, but
484 WITHOUT ANY WARRANTY; without even the implied warranty of
485 MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
486 General Public License for more details.
488 You should have received a copy of the GNU General Public License along
489 with jack_mixer; if not, write to the Free Software Foundation, Inc., 51
490 Franklin Street, Fifth Floor, Boston, MA 02110-130159 USA"""
494 "Nedko Arnaudov <nedko@arnaudov.name>",
495 "Christopher Arndt <chris@chrisarndt.de>",
496 "Arnout Engelen <arnouten@bzzt.net>",
497 "John Hedges <john@drystone.co.uk>",
498 "Olivier Humbert <trebmuh@tuxfamily.org>",
499 "Sarah Mischke <sarah@spooky-online.de>",
500 "Frédéric Péters <fpeters@0d.be>",
501 "Daniel Sheeler <dsheeler@pobox.com>",
502 "Athanasios Silis <athanasios.silis@gmail.com>",
505 about
.set_logo_icon_name("jack_mixer")
506 about
.set_version(__version__
)
507 about
.set_website("https://rdio.space/jackmixer/")
511 def on_delete_event(self
, widget
, event
):
516 return self
.on_quit_cb(on_delete
=True)
518 def add_file_filters(self
, dialog
):
519 filter_xml
= Gtk
.FileFilter()
520 filter_xml
.set_name("XML files")
521 filter_xml
.add_mime_type("text/xml")
522 dialog
.add_filter(filter_xml
)
523 filter_all
= Gtk
.FileFilter()
524 filter_all
.set_name("All files")
525 filter_all
.add_pattern("*")
526 dialog
.add_filter(filter_all
)
528 def _open_project(self
, filename
):
530 with
open(filename
, "r") as fp
:
531 self
.load_from_xml(fp
)
532 except Exception as exc
:
533 error_dialog(self
.window
, "Error loading project file '%s': %s", filename
, exc
)
535 self
.current_filename
= filename
538 def on_open_cb(self
, *args
):
539 dlg
= Gtk
.FileChooserDialog(
540 title
="Open project", parent
=self
.window
, action
=Gtk
.FileChooserAction
.OPEN
543 Gtk
.STOCK_CANCEL
, Gtk
.ResponseType
.CANCEL
, Gtk
.STOCK_OPEN
, Gtk
.ResponseType
.OK
545 dlg
.set_default_response(Gtk
.ResponseType
.OK
)
547 default_project_path
= self
.gui_factory
.get_default_project_path()
549 if self
.current_filename
:
550 dlg
.set_current_folder(os
.path
.dirname(self
.current_filename
))
552 dlg
.set_current_folder(self
.last_project_path
or default_project_path
or os
.getcwd())
554 if default_project_path
:
555 dlg
.add_shortcut_folder(default_project_path
)
557 self
.add_file_filters(dlg
)
559 if dlg
.run() == Gtk
.ResponseType
.OK
:
560 filename
= dlg
.get_filename()
561 if self
._open
_project
(filename
):
562 self
.recentmanager
.add_item("file://" + os
.path
.abspath(filename
))
566 def on_recent_file_chosen(self
, recentchooser
):
567 item
= recentchooser
.get_current_item()
569 if item
and item
.exists():
570 log
.debug("Recent file menu entry selected: %s", item
.get_display_name())
572 if not self
._open
_project
(urlparse(uri
).path
):
573 self
.recentmanager
.remove_item(uri
)
575 def _save_project(self
, filename
):
576 with
open(filename
, "w") as fp
:
579 def on_save_cb(self
, *args
):
580 if not self
.current_filename
:
581 return self
.on_save_as_cb()
584 self
._save
_project
(self
.current_filename
)
585 except Exception as exc
:
587 self
.window
, "Error saving project file '%s': %s", self
.current_filename
, exc
590 def on_save_as_cb(self
, *args
):
591 dlg
= Gtk
.FileChooserDialog(
592 title
="Save project", parent
=self
.window
, action
=Gtk
.FileChooserAction
.SAVE
595 Gtk
.STOCK_CANCEL
, Gtk
.ResponseType
.CANCEL
, Gtk
.STOCK_SAVE
, Gtk
.ResponseType
.OK
597 dlg
.set_default_response(Gtk
.ResponseType
.OK
)
598 dlg
.set_do_overwrite_confirmation(True)
600 default_project_path
= self
.gui_factory
.get_default_project_path()
602 if self
.current_filename
:
603 dlg
.set_filename(self
.current_filename
)
605 dlg
.set_current_folder(self
.last_project_path
or default_project_path
or os
.getcwd())
606 filename
= "{}-{}.xml".format(
607 getpass
.getuser(), datetime
.datetime
.now().strftime("%Y%m%d-%H%M")
609 dlg
.set_current_name(filename
)
611 if default_project_path
:
612 dlg
.add_shortcut_folder(default_project_path
)
614 self
.add_file_filters(dlg
)
616 if dlg
.run() == Gtk
.ResponseType
.OK
:
617 save_path
= dlg
.get_filename()
618 save_dir
= os
.path
.dirname(save_path
)
619 if os
.path
.isdir(save_dir
):
620 self
.last_project_path
= save_dir
622 filename
= dlg
.get_filename()
624 self
._save
_project
(filename
)
625 except Exception as exc
:
626 error_dialog(self
.window
, "Error saving project file '%s': %s", filename
, exc
)
628 self
.current_filename
= filename
629 self
.recentmanager
.add_item("file://" + os
.path
.abspath(filename
))
633 def on_quit_cb(self
, *args
, on_delete
=False):
634 if not self
.nsm_client
and self
.gui_factory
.get_confirm_quit():
635 dlg
= Gtk
.MessageDialog(
637 message_type
=Gtk
.MessageType
.QUESTION
,
638 buttons
=Gtk
.ButtonsType
.NONE
,
640 dlg
.set_markup("<b>Quit application?</b>")
641 dlg
.format_secondary_markup(
642 "All jack_mixer ports will be closed and connections lost,"
643 "\nstopping all sound going through jack_mixer.\n\n"
647 Gtk
.STOCK_CANCEL
, Gtk
.ResponseType
.CANCEL
, Gtk
.STOCK_QUIT
, Gtk
.ResponseType
.OK
651 if response
!= Gtk
.ResponseType
.OK
:
656 def on_shrink_channels_cb(self
, widget
):
657 for channel
in self
.channels
+ self
.output_channels
:
660 def on_expand_channels_cb(self
, widget
):
661 for channel
in self
.channels
+ self
.output_channels
:
664 def on_midi_behavior_mode_changed(self
, gui_factory
, value
):
665 self
.mixer
.midi_behavior_mode
= value
667 def on_preferences_cb(self
, widget
):
668 if not self
.preferences_dialog
:
669 self
.preferences_dialog
= PreferencesDialog(self
)
670 self
.preferences_dialog
.show()
671 self
.preferences_dialog
.present()
673 def on_add_channel(self
, inout
="input", default_name
="Input"):
674 dialog
= getattr(self
, "_add_{}_dialog".format(inout
), None)
675 values
= getattr(self
, "_add_{}_values".format(inout
), {})
678 cls
= NewInputChannelDialog
if inout
== "input" else NewOutputChannelDialog
679 dialog
= cls(app
=self
)
680 setattr(self
, "_add_{}_dialog".format(inout
), dialog
)
683 ch
.channel_name
for ch
in (self
.channels
if inout
== "input" else self
.output_channels
)
685 values
.setdefault("name", default_name
)
687 if values
["name"] in names
:
688 values
["name"] = add_number_suffix(values
["name"])
692 dialog
.fill_ui(**values
)
693 dialog
.set_transient_for(self
.window
)
698 if ret
== Gtk
.ResponseType
.OK
:
699 result
= dialog
.get_result()
700 setattr(self
, "_add_{}_values".format(inout
), result
)
701 (self
.add_channel
if inout
== "input" else self
.add_output_channel
)(**result
)
702 if self
.visible
or self
.nsm_client
is None:
703 self
.window
.show_all()
705 def on_add_input_channel(self
, widget
):
706 return self
.on_add_channel("input", "Input")
708 def on_add_output_channel(self
, widget
):
709 return self
.on_add_channel("output", "Output")
711 def on_edit_input_channel(self
, widget
, channel
):
712 log
.debug('Editing input channel "%s".', channel
.channel_name
)
713 channel
.on_channel_properties()
715 def on_remove_input_channel(self
, widget
, channel
):
716 log
.debug('Removing input channel "%s".', channel
.channel_name
)
718 def remove_channel_edit_input_menuitem_by_label(widget
, label
):
719 if widget
.get_label() == label
:
720 self
.channel_edit_input_menu
.remove(widget
)
722 self
.channel_remove_input_menu
.remove(widget
)
723 self
.channel_edit_input_menu
.foreach(
724 remove_channel_edit_input_menuitem_by_label
, channel
.channel_name
727 if self
.monitored_channel
is channel
:
728 channel
.monitor_button
.set_active(False)
730 for i
in range(len(self
.channels
)):
731 if self
.channels
[i
] is channel
:
734 self
.hbox_inputs
.remove(channel
.get_parent())
737 if not self
.channels
:
738 self
.channel_edit_input_menu_item
.set_sensitive(False)
739 self
.channel_remove_input_menu_item
.set_sensitive(False)
741 def on_edit_output_channel(self
, widget
, channel
):
742 log
.debug('Editing output channel "%s".', channel
.channel_name
)
743 channel
.on_channel_properties()
745 def on_remove_output_channel(self
, widget
, channel
):
746 log
.debug('Removing output channel "%s".', channel
.channel_name
)
748 def remove_channel_edit_output_menuitem_by_label(widget
, label
):
749 if widget
.get_label() == label
:
750 self
.channel_edit_output_menu
.remove(widget
)
752 self
.channel_remove_output_menu
.remove(widget
)
753 self
.channel_edit_output_menu
.foreach(
754 remove_channel_edit_output_menuitem_by_label
, channel
.channel_name
757 if self
.monitored_channel
is channel
:
758 channel
.monitor_button
.set_active(False)
760 for i
in range(len(self
.channels
)):
761 if self
.output_channels
[i
] is channel
:
763 del self
.output_channels
[i
]
764 self
.hbox_outputs
.remove(channel
.get_parent())
767 if not self
.output_channels
:
768 self
.channel_edit_output_menu_item
.set_sensitive(False)
769 self
.channel_remove_output_menu_item
.set_sensitive(False)
771 def on_channel_rename(self
, oldname
, newname
):
772 def rename_channels(container
, parameters
):
773 if container
.get_label() == parameters
["oldname"]:
774 container
.set_label(parameters
["newname"])
776 rename_parameters
= {"oldname": oldname
, "newname": newname
}
777 self
.channel_edit_input_menu
.foreach(rename_channels
, rename_parameters
)
778 self
.channel_edit_output_menu
.foreach(rename_channels
, rename_parameters
)
779 self
.channel_remove_input_menu
.foreach(rename_channels
, rename_parameters
)
780 self
.channel_remove_output_menu
.foreach(rename_channels
, rename_parameters
)
781 log
.debug('Renaming channel from "%s" to "%s".', oldname
, newname
)
783 def reorder_menu_item(self
, menu
, source_label
, dest_label
):
786 for i
, menuitem
in enumerate(menu
.get_children()):
787 label
= menuitem
.get_label()
788 if label
== source_label
:
789 source_item
= menuitem
790 elif label
== dest_label
:
793 if pos
!= -1 and source_item
is not None:
794 menu
.reorder_child(source_item
, pos
)
796 def reorder_channels(self
, container
, source_name
, dest_name
, reverse
=False):
797 frames
= container
.get_children()
800 if source_name
== frame
.get_child().channel_name
:
807 for pos
, frame
in enumerate(frames
):
808 if dest_name
== frame
.get_child().channel_name
:
809 container
.reorder_child(source_frame
, pos
)
812 def on_input_channel_order_changed(self
, widget
, source_name
, dest_name
):
813 self
.channels
.clear()
814 self
.reorder_channels(self
.hbox_inputs
, source_name
, dest_name
)
816 for frame
in self
.hbox_inputs
.get_children():
817 self
.channels
.append(frame
.get_child())
819 for menu
in (self
.channel_edit_input_menu
, self
.channel_remove_input_menu
):
820 self
.reorder_menu_item(menu
, source_name
, dest_name
)
822 def on_output_channel_order_changed(self
, widget
, source_name
, dest_name
):
823 self
.output_channels
.clear()
824 self
.reorder_channels(self
.hbox_outputs
, source_name
, dest_name
, reverse
=True)
826 for frame
in self
.hbox_outputs
.get_children():
827 self
.output_channels
.append(frame
.get_child())
829 for menu
in (self
.channel_edit_output_menu
, self
.channel_remove_output_menu
):
830 self
.reorder_menu_item(menu
, source_name
, dest_name
)
832 def on_channels_clear(self
, widget
):
833 dlg
= Gtk
.MessageDialog(
836 message_type
=Gtk
.MessageType
.WARNING
,
837 text
="Are you sure you want to clear all channels?",
838 buttons
=Gtk
.ButtonsType
.OK_CANCEL
,
841 if not widget
or dlg
.run() == Gtk
.ResponseType
.OK
:
842 for channel
in self
.output_channels
:
844 self
.hbox_outputs
.remove(channel
.get_parent())
845 for channel
in self
.channels
:
847 self
.hbox_inputs
.remove(channel
.get_parent())
849 self
.output_channels
= []
850 self
.channel_edit_input_menu
= Gtk
.Menu()
851 self
.channel_edit_input_menu_item
.set_submenu(self
.channel_edit_input_menu
)
852 self
.channel_edit_input_menu_item
.set_sensitive(False)
853 self
.channel_remove_input_menu
= Gtk
.Menu()
854 self
.channel_remove_input_menu_item
.set_submenu(self
.channel_remove_input_menu
)
855 self
.channel_remove_input_menu_item
.set_sensitive(False)
856 self
.channel_edit_output_menu
= Gtk
.Menu()
857 self
.channel_edit_output_menu_item
.set_submenu(self
.channel_edit_output_menu
)
858 self
.channel_edit_output_menu_item
.set_sensitive(False)
859 self
.channel_remove_output_menu
= Gtk
.Menu()
860 self
.channel_remove_output_menu_item
.set_submenu(self
.channel_remove_output_menu
)
861 self
.channel_remove_output_menu_item
.set_sensitive(False)
863 # Force save-as dialog on next save
864 self
.current_filename
= None
868 def read_meters(self
):
869 for channel
in self
.channels
:
871 for channel
in self
.output_channels
:
875 def midi_events_check(self
):
876 for channel
in self
.channels
+ self
.output_channels
:
877 channel
.midi_events_check()
880 def get_monitored_channel(self
):
881 return self
._monitored
_channel
883 def set_monitored_channel(self
, channel
):
884 if channel
== self
._monitored
_channel
:
886 self
._monitored
_channel
= channel
888 self
.monitor_channel
.out_mute
= True
889 elif isinstance(channel
, InputChannel
):
890 # reset all solo/mute settings
891 for in_channel
in self
.channels
:
892 self
.monitor_channel
.set_solo(in_channel
.channel
, False)
893 self
.monitor_channel
.set_muted(in_channel
.channel
, False)
894 self
.monitor_channel
.set_solo(channel
.channel
, True)
895 self
.monitor_channel
.prefader
= True
896 self
.monitor_channel
.out_mute
= False
898 self
.monitor_channel
.prefader
= False
899 self
.monitor_channel
.out_mute
= False
902 self
.update_monitor(channel
)
904 monitored_channel
= property(get_monitored_channel
, set_monitored_channel
)
906 def update_monitor(self
, channel
):
907 if self
._monitored
_channel
is not channel
:
909 self
.monitor_channel
.volume
= channel
.channel
.volume
910 self
.monitor_channel
.balance
= channel
.channel
.balance
911 if isinstance(self
.monitored_channel
, OutputChannel
):
912 # sync solo/muted channels
913 for input_channel
in self
.channels
:
914 self
.monitor_channel
.set_solo(
915 input_channel
.channel
, channel
.channel
.is_solo(input_channel
.channel
)
917 self
.monitor_channel
.set_muted(
918 input_channel
.channel
, channel
.channel
.is_muted(input_channel
.channel
)
921 def get_input_channel_by_name(self
, name
):
922 for input_channel
in self
.channels
:
923 if input_channel
.channel
.name
== name
:
927 # ---------------------------------------------------------------------------------------------
928 # Mixer project (de-)serialization and file handling
930 def save_to_xml(self
, file):
931 log
.debug("Saving to XML...")
932 b
= XmlSerialization()
937 def load_from_xml(self
, file, silence_errors
=False, from_nsm
=False):
938 log
.debug("Loading from XML...")
939 self
.unserialized_channels
= []
940 b
= XmlSerialization()
942 b
.load(file, self
.serialization_name())
948 self
.on_channels_clear(None)
950 s
.unserialize(self
, b
)
951 for channel
in self
.unserialized_channels
:
952 if isinstance(channel
, InputChannel
):
953 if self
._init
_solo
_channels
and channel
.channel_name
in self
._init
_solo
_channels
:
955 self
.add_channel_precreated(channel
)
956 self
._init
_solo
_channels
= None
957 for channel
in self
.unserialized_channels
:
958 if isinstance(channel
, OutputChannel
):
959 self
.add_output_channel_precreated(channel
)
960 del self
.unserialized_channels
961 width
, height
= self
.window
.get_size()
962 if self
.visible
or not from_nsm
:
963 self
.window
.show_all()
965 if self
.output_channels
:
966 self
.output_channels
[-1].volume_digits
.select_region(0, 0)
967 self
.output_channels
[-1].slider
.grab_focus()
969 self
.channels
[-1].volume_digits
.select_region(0, 0)
970 self
.channels
[-1].volume_digits
.grab_focus()
972 self
.paned
.set_position(self
.paned_position
/ self
.width
* width
)
973 self
.window
.resize(self
.width
, self
.height
)
975 def serialize(self
, object_backend
):
976 width
, height
= self
.window
.get_size()
977 object_backend
.add_property("geometry", "%sx%s" % (width
, height
))
978 pos
= self
.paned
.get_position()
979 object_backend
.add_property("paned_position", "%s" % pos
)
981 for input_channel
in self
.channels
:
982 if input_channel
.channel
.solo
:
983 solo_channels
.append(input_channel
)
985 object_backend
.add_property(
986 "solo_channels", "|".join([x
.channel
.name
for x
in solo_channels
])
988 object_backend
.add_property("visible", "%s" % str(self
.visible
))
990 def unserialize_property(self
, name
, value
):
991 if name
== "geometry":
992 width
, height
= value
.split("x")
993 self
.width
= int(width
)
994 self
.height
= int(height
)
996 if name
== "solo_channels":
997 self
._init
_solo
_channels
= value
.split("|")
999 if name
== "visible":
1000 self
.visible
= value
== "True"
1002 if name
== "paned_position":
1003 self
.paned_position
= int(value
)
1007 def unserialize_child(self
, name
):
1008 if name
== InputChannel
.serialization_name():
1009 channel
= InputChannel(self
, "", True)
1010 self
.unserialized_channels
.append(channel
)
1013 if name
== OutputChannel
.serialization_name():
1014 channel
= OutputChannel(self
, "", True)
1015 self
.unserialized_channels
.append(channel
)
1018 if name
== gui
.Factory
.serialization_name():
1019 return self
.gui_factory
1021 def serialization_get_childs(self
):
1022 """Get child objects that required and support serialization"""
1023 childs
= self
.channels
[:] + self
.output_channels
[:] + [self
.gui_factory
]
1026 def serialization_name(self
):
1029 # ---------------------------------------------------------------------------------------------
1036 if self
.visible
or self
.nsm_client
is None:
1037 width
, height
= self
.window
.get_size()
1038 self
.window
.show_all()
1039 if hasattr(self
, "paned_position"):
1040 self
.paned
.set_position(self
.paned_position
/ self
.width
* width
)
1042 signal
.signal(signal
.SIGUSR1
, self
.sighandler
)
1043 signal
.signal(signal
.SIGTERM
, self
.sighandler
)
1044 signal
.signal(signal
.SIGINT
, self
.sighandler
)
1045 signal
.signal(signal
.SIGHUP
, signal
.SIG_IGN
)
1050 def error_dialog(parent
, msg
, *args
):
1051 log
.exception(msg
, *args
)
1052 err
= Gtk
.MessageDialog(
1055 destroy_with_parent
=True,
1056 message_type
=Gtk
.MessageType
.ERROR
,
1057 buttons
=Gtk
.ButtonsType
.OK
,
1065 parser
= ArgumentParser()
1066 parser
.add_argument(
1067 "-c", "--config", metavar
="FILE", help="load mixer project configuration from FILE"
1069 parser
.add_argument("-d", "--debug", action
="store_true", help="enable debug logging messages")
1070 parser
.add_argument(
1071 "client_name", metavar
="NAME", nargs
="?", default
="jack_mixer", help="set JACK client name"
1073 args
= parser
.parse_args()
1075 logging
.basicConfig(
1076 level
=logging
.DEBUG
if args
.debug
else logging
.INFO
, format
="%(levelname)s: %(message)s"
1080 mixer
= JackMixer(args
.client_name
)
1081 except Exception as e
:
1082 error_dialog(None, "Mixer creation failed:\n\n%s", e
)
1085 if not mixer
.nsm_client
and args
.config
:
1087 with
open(args
.config
) as fp
:
1088 mixer
.load_from_xml(fp
)
1089 except Exception as exc
:
1090 error_dialog(mixer
.window
, "Error loading project file '%s': %s", args
.config
, exc
)
1092 mixer
.current_filename
= args
.config
1094 mixer
.window
.set_default_size(
1095 60 * (1 + len(mixer
.channels
) + len(mixer
.output_channels
)), 300
1103 if __name__
== "__main__":