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.
27 from argparse
import ArgumentParser
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/)
38 sys
.path
.insert(0, os
.path
.join(os
.path
.dirname(sys
.argv
[0]), "..", "share", "jack_mixer"))
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
54 log
= logging
.getLogger("jack_mixer")
57 def add_number_suffix(s
):
59 return str(int(match
.group(0)) + 1)
61 new_s
= re
.sub(r
"(\d+)\s*$", inc
, s
)
68 class JackMixer(SerializedObject
):
70 # scales suitable as meter scales
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"):
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
,
101 self
.nsm_client
.announceGuiVisibility(self
.visible
)
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
)
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
)
120 GLib
.timeout_add(200, self
.nsm_react
)
123 log
.debug("Cleaning jack_mixer.")
127 for channel
in self
.channels
:
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
)
139 menuitem
.connect("activate", callback
)
141 key
, mod
= Gtk
.accelerator_parse(accel
)
142 menuitem
.add_accelerator(
143 "activate", self
.menu_accelgroup
, key
, mod
, Gtk
.AccelFlags
.VISIBLE
147 def create_ui(self
, with_nsm
):
149 self
.output_channels
= []
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
)
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(
186 "New Output _Channel", self
.on_add_output_channel
, "<Shift><Control>N"
190 self
.mixer_menu
.append(Gtk
.SeparatorMenuItem())
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"))
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())
203 self
.mixer_menu
.append(self
.new_menu_item("_Hide", self
.nsm_hide_cb
, "<Control>W"))
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())
240 self
.new_menu_item("Shrink Channels", self
.on_shrink_channels_cb
, "<Control>minus")
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 # ---------------------------------------------------------------------------------------------
288 def add_channel(self
, name
, stereo
, volume_cc
, balance_cc
, mute_cc
, solo_cc
, value
):
290 channel
= InputChannel(self
, name
, stereo
, value
)
291 self
.add_channel_precreated(channel
)
293 error_dialog(self
.window
, "Input channel creation failed.")
296 channel
.assign_midi_ccs(volume_cc
, balance_cc
, mute_cc
, solo_cc
)
299 def add_channel_precreated(self
, channel
):
302 self
.hbox_inputs
.pack_start(frame
, False, True, 0)
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
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
)
338 error_dialog(self
.window
, "Output channel creation failed")
341 channel
.assign_midi_ccs(volume_cc
, balance_cc
, mute_cc
)
344 def add_output_channel_precreated(self
, channel
):
347 self
.hbox_outputs
.pack_end(frame
, False, True, 0)
348 self
.hbox_outputs
.reorder_child(frame
, 0)
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 # ---------------------------------------------------------------------------------------------
371 self
.nsm_client
.reactToMessage()
374 def nsm_hide_cb(self
, *args
):
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
)
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
):
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
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")
406 def nsm_exit_cb(self
, path
, session_name
, client_name
):
409 # ---------------------------------------------------------------------------------------------
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
)
419 log
.warning("Unknown signal %d received.", signum
)
421 # ---------------------------------------------------------------------------------------------
424 def on_about(self
, *args
):
425 about
= Gtk
.AboutDialog()
426 about
.set_name("jack_mixer")
427 about
.set_program_name("jack_mixer")
429 "Copyright © 2006-2020\n"
430 "Nedko Arnaudov, Frédéric Péters, Arnout Engelen, Daniel Sheeler"
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"""
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/")
467 def on_delete_event(self
, widget
, event
):
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
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()
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
)
490 self
.current_filename
= filename
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")
500 def on_save_as_cb(self
, *args
):
501 dlg
= Gtk
.FileChooserDialog(
502 title
="Save", parent
=self
.window
, action
=Gtk
.FileChooserAction
.SAVE
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()
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(
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"
531 Gtk
.STOCK_CANCEL
, Gtk
.ResponseType
.CANCEL
, Gtk
.STOCK_QUIT
, Gtk
.ResponseType
.OK
535 if response
!= Gtk
.ResponseType
.OK
:
540 def on_shrink_channels_cb(self
, widget
):
541 for channel
in self
.channels
+ self
.output_channels
:
544 def on_expand_channels_cb(self
, widget
):
545 for channel
in self
.channels
+ self
.output_channels
:
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
), {})
562 cls
= NewInputChannelDialog
if inout
== "input" else NewOutputChannelDialog
563 dialog
= cls(app
=self
)
564 setattr(self
, "_add_{}_dialog".format(inout
), dialog
)
567 ch
.channel_name
for ch
in (self
.channels
if inout
== "input" else self
.output_channels
)
569 values
.setdefault("name", default_name
)
571 if values
["name"] in names
:
572 values
["name"] = add_number_suffix(values
["name"])
576 dialog
.fill_ui(**values
)
577 dialog
.set_transient_for(self
.window
)
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
:
618 self
.hbox_inputs
.remove(channel
.get_parent())
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
:
647 del self
.output_channels
[i
]
648 self
.hbox_outputs
.remove(channel
.get_parent())
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()
675 if source_name
== c
._channel
_name
:
681 if dest_name
== c
._channel
_name
:
682 pos
= frames
.index(f
)
683 channel_box
.reorder_child(source_frame
, pos
)
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()
698 if source_name
== c
._channel
_name
:
704 if dest_name
== c
._channel
_name
:
705 pos
= len(frames
) - 1 - frames
.index(f
)
706 channel_box
.reorder_child(source_frame
, pos
)
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(
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
:
724 self
.hbox_outputs
.remove(channel
.get_parent())
725 for channel
in self
.channels
:
727 self
.hbox_inputs
.remove(channel
.get_parent())
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)
744 def read_meters(self
):
745 for channel
in self
.channels
:
747 for channel
in self
.output_channels
:
751 def midi_events_check(self
):
752 for channel
in self
.channels
+ self
.output_channels
:
753 channel
.midi_events_check()
756 def get_monitored_channel(self
):
757 return self
._monitored
_channel
759 def set_monitored_channel(self
, channel
):
760 if channel
== self
._monitored
_channel
:
762 self
._monitored
_channel
= channel
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
774 self
.monitor_channel
.prefader
= False
775 self
.monitor_channel
.out_mute
= False
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
:
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
:
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()
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()
823 self
.on_channels_clear(None)
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
:
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()
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
)
856 for input_channel
in self
.channels
:
857 if input_channel
.channel
.solo
:
858 solo_channels
.append(input_channel
)
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
)
871 if name
== "solo_channels":
872 self
._init
_solo
_channels
= value
.split("|")
874 if name
== "visible":
875 self
.visible
= value
== "True"
877 if name
== "paned_position":
878 self
.paned_position
= int(value
)
882 def unserialize_child(self
, name
):
883 if name
== InputChannel
.serialization_name():
884 channel
= InputChannel(self
, "", True)
885 self
.unserialized_channels
.append(channel
)
888 if name
== OutputChannel
.serialization_name():
889 channel
= OutputChannel(self
, "", True)
890 self
.unserialized_channels
.append(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
]
901 def serialization_name(self
):
904 # ---------------------------------------------------------------------------------------------
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
)
925 def error_dialog(parent
, msg
, *args
):
926 log
.exception(msg
, *args
)
927 err
= Gtk
.MessageDialog(
930 destroy_with_parent
=True,
931 message_type
=Gtk
.MessageType
.ERROR
,
932 buttons
=Gtk
.ButtonsType
.OK
,
940 parser
= ArgumentParser()
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")
946 "client_name", metavar
="NAME", nargs
="?", default
="jack_mixer", help="set JACK client name"
948 args
= parser
.parse_args()
951 level
=logging
.DEBUG
if args
.debug
else logging
.INFO
, format
="%(levelname)s: %(message)s"
955 mixer
= JackMixer(args
.client_name
)
956 except Exception as e
:
957 error_dialog(None, "Mixer creation failed:\n\n%s", e
)
960 if not mixer
.nsm_client
and args
.config
:
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
)
967 mixer
.current_filename
= args
.config
969 mixer
.window
.set_default_size(
970 60 * (1 + len(mixer
.channels
) + len(mixer
.output_channels
)), 300
978 if __name__
== "__main__":