3 # This file is part of jack_mixer
5 # Copyright (C) 2006 Nedko Arnaudov <nedko@arnaudov.name>
7 # This program is free software; you can redistribute it and/or modify
8 # it under the terms of the GNU General Public License as published by
9 # the Free Software Foundation; version 2 of the License
11 # This program is distributed in the hope that it will be useful,
12 # but WITHOUT ANY WARRANTY; without even the implied warranty of
13 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
14 # GNU General Public License for more details.
16 # You should have received a copy of the GNU General Public License
17 # along with this program; if not, write to the Free Software
18 # Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA.
20 from optparse import OptionParser
34 sys.path.insert(0, os.path.dirname(sys.argv[0]) + os.sep + ".." + os.sep + "share"+ os.sep + "jack_mixer")
37 from preferences import PreferencesDialog
42 from serialization_xml import xml_serialization
43 from serialization import serialized_object, serializator
45 xml_serialization = None
47 if lash is None or xml_serialization is None:
48 print >> sys.stderr, "Cannot load LASH python bindings or python-xml, you want them unless you enjoy manual jack plumbing each time you use this app"
50 class jack_mixer(serialized_object):
52 # scales suitable as meter scales
53 meter_scales = [scale.iec_268(), scale.linear_70dB(), scale.iec_268_minimalistic()]
55 # scales suitable as volume slider scales
56 slider_scales = [scale.linear_30dB(), scale.linear_70dB()]
58 # name of settngs file that is currently open
59 current_filename = None
61 def __init__(self, name, lash_client):
62 self.mixer = jack_mixer_c.Mixer(name)
67 # Send our client name to server
68 lash_event = lash.lash_event_new_with_type(lash.LASH_Client_Name)
69 lash.lash_event_set_string(lash_event, name)
70 lash.lash_send_event(lash_client, lash_event)
72 lash.lash_jack_client_name(lash_client, name)
74 gtk.window_set_default_icon_name('jack_mixer')
76 self.window = gtk.Window(gtk.WINDOW_TOPLEVEL)
77 self.window.set_title(name)
79 self.gui_factory = gui.factory(self.window, self.meter_scales, self.slider_scales)
81 self.vbox_top = gtk.VBox()
82 self.window.add(self.vbox_top)
84 self.menubar = gtk.MenuBar()
85 self.vbox_top.pack_start(self.menubar, False)
87 mixer_menu_item = gtk.MenuItem("_Mixer")
88 self.menubar.append(mixer_menu_item)
89 edit_menu_item = gtk.MenuItem('_Edit')
90 self.menubar.append(edit_menu_item)
92 self.window.set_default_size(120,300)
94 mixer_menu = gtk.Menu()
95 mixer_menu_item.set_submenu(mixer_menu)
97 add_input_channel = gtk.ImageMenuItem('New _Input Channel')
98 mixer_menu.append(add_input_channel)
99 add_input_channel.connect("activate", self.on_add_input_channel)
101 add_output_channel = gtk.ImageMenuItem('New _Output Channel')
102 mixer_menu.append(add_output_channel)
103 add_output_channel.connect("activate", self.on_add_output_channel)
105 if lash_client is None and xml_serialization is not None:
106 mixer_menu.append(gtk.SeparatorMenuItem())
107 open = gtk.ImageMenuItem(gtk.STOCK_OPEN)
108 mixer_menu.append(open)
109 open.connect('activate', self.on_open_cb)
110 save = gtk.ImageMenuItem(gtk.STOCK_SAVE)
111 mixer_menu.append(save)
112 save.connect('activate', self.on_save_cb)
113 save_as = gtk.ImageMenuItem(gtk.STOCK_SAVE_AS)
114 mixer_menu.append(save_as)
115 save_as.connect('activate', self.on_save_as_cb)
117 mixer_menu.append(gtk.SeparatorMenuItem())
119 quit = gtk.ImageMenuItem(gtk.STOCK_QUIT)
120 mixer_menu.append(quit)
121 quit.connect('activate', self.on_quit_cb)
123 edit_menu = gtk.Menu()
124 edit_menu_item.set_submenu(edit_menu)
126 self.channel_remove_menu_item = gtk.ImageMenuItem(gtk.STOCK_REMOVE)
127 edit_menu.append(self.channel_remove_menu_item)
128 self.channel_remove_menu = gtk.Menu()
129 self.channel_remove_menu_item.set_submenu(self.channel_remove_menu)
131 channel_remove_all_menu_item = gtk.ImageMenuItem(gtk.STOCK_CLEAR)
132 edit_menu.append(channel_remove_all_menu_item)
133 channel_remove_all_menu_item.connect("activate", self.on_channels_clear)
135 edit_menu.append(gtk.SeparatorMenuItem())
137 preferences = gtk.ImageMenuItem(gtk.STOCK_PREFERENCES)
138 preferences.connect('activate', self.on_preferences_cb)
139 edit_menu.append(preferences)
141 self.hbox_top = gtk.HBox()
142 self.vbox_top.pack_start(self.hbox_top, True)
144 self.scrolled_window = gtk.ScrolledWindow()
145 self.hbox_top.pack_start(self.scrolled_window, True)
147 self.hbox_inputs = gtk.HBox()
148 self.hbox_inputs.set_spacing(0)
149 self.hbox_inputs.set_border_width(0)
150 self.hbox_top.set_spacing(0)
151 self.hbox_top.set_border_width(0)
153 self.output_channels = []
155 self.scrolled_window.set_policy(gtk.POLICY_AUTOMATIC, gtk.POLICY_AUTOMATIC)
156 self.scrolled_window.add_with_viewport(self.hbox_inputs)
158 self.main_mix = main_mix(self)
159 self.main_mix.realize()
160 self.hbox_outputs = gtk.HBox()
161 self.hbox_outputs.set_spacing(0)
162 self.hbox_outputs.set_border_width(0)
164 frame.add(self.main_mix)
165 self.hbox_outputs.pack_start(frame, False)
166 self.hbox_top.pack_start(self.hbox_outputs, False)
168 self.window.connect("destroy", gtk.main_quit)
170 gobject.timeout_add(80, self.read_meters)
171 self.lash_client = lash_client
174 gobject.timeout_add(1000, self.lash_check_events)
177 print "Cleaning jack_mixer"
181 for channel in self.channels:
184 def on_open_cb(self, *args):
185 dlg = gtk.FileChooserDialog(title='Open', parent=self.window,
186 action=gtk.FILE_CHOOSER_ACTION_OPEN,
187 buttons=(gtk.STOCK_CANCEL, gtk.RESPONSE_CANCEL,
188 gtk.STOCK_OPEN, gtk.RESPONSE_OK))
189 dlg.set_default_response(gtk.RESPONSE_OK)
190 if dlg.run() == gtk.RESPONSE_OK:
191 filename = dlg.get_filename()
193 f = file(filename, 'r')
194 self.load_from_xml(f)
196 # TODO: display error in a dialog box
197 print >> sys.stderr, 'Failed to read', filename
199 self.current_filename = filename
204 def on_save_cb(self, *args):
205 if not self.current_filename:
206 return self.on_save_as_cb()
207 f = file(self.current_filename, 'w')
211 def on_save_as_cb(self, *args):
212 dlg = gtk.FileChooserDialog(title='Save', parent=self.window,
213 action=gtk.FILE_CHOOSER_ACTION_SAVE,
214 buttons=(gtk.STOCK_CANCEL, gtk.RESPONSE_CANCEL,
215 gtk.STOCK_SAVE, gtk.RESPONSE_OK))
216 dlg.set_default_response(gtk.RESPONSE_OK)
217 if dlg.run() == gtk.RESPONSE_OK:
218 self.current_filename = dlg.get_filename()
222 def on_quit_cb(self, *args):
225 preferences_dialog = None
226 def on_preferences_cb(self, widget):
227 if not self.preferences_dialog:
228 self.preferences_dialog = PreferencesDialog(self)
229 self.preferences_dialog.show()
230 self.preferences_dialog.present()
232 def on_add_input_channel(self, widget):
233 dialog = NewChannelDialog(parent=self.window, mixer=self.mixer)
234 dialog.set_transient_for(self.window)
239 if ret == gtk.RESPONSE_OK:
240 result = dialog.get_result()
241 channel = self.add_channel(**result)
242 self.window.show_all()
244 def on_add_output_channel(self, widget):
245 dialog = NewOutputChannelDialog(parent=self.window, mixer=self.mixer)
246 dialog.set_transient_for(self.window)
251 if ret == gtk.RESPONSE_OK:
252 result = dialog.get_result()
253 channel = self.add_output_channel(**result)
254 self.window.show_all()
256 def on_remove_channel(self, widget, channel):
257 print 'Removing channel "%s"' % channel.channel_name
258 self.channel_remove_menu.remove(widget)
259 for i in range(len(self.channels)):
260 if self.channels[i] is channel:
263 self.hbox_inputs.remove(channel.parent)
265 if len(self.channels) == 0:
266 self.channel_remove_menu_item.set_sensitive(False)
268 def on_channels_clear(self, widget):
269 for channel in self.channels:
271 self.hbox_inputs.remove(channel.parent)
273 self.channel_remove_menu = gtk.Menu()
274 self.channel_remove_menu_item.set_submenu(self.channel_remove_menu)
275 self.channel_remove_menu_item.set_sensitive(False)
277 def add_channel(self, name, stereo, volume_cc, balance_cc):
279 channel = input_channel(self, name, stereo)
280 self.add_channel_precreated(channel)
282 err = gtk.MessageDialog(self.window,
283 gtk.DIALOG_MODAL | gtk.DIALOG_DESTROY_WITH_PARENT,
286 "Channel creation failed")
291 channel.channel.volume_midi_cc = int(volume_cc)
293 channel.channel.balance_midi_cc = int(balance_cc)
294 if not (volume_cc or balance_cc):
295 channel.channel.autoset_midi_cc()
298 def add_channel_precreated(self, channel):
301 self.hbox_inputs.pack_start(frame, False)
303 channel_remove_menu_item = gtk.MenuItem(channel.channel_name)
304 self.channel_remove_menu.append(channel_remove_menu_item)
305 channel_remove_menu_item.connect("activate", self.on_remove_channel, channel)
306 self.channel_remove_menu_item.set_sensitive(True)
307 self.channels.append(channel)
309 for outputchannel in self.output_channels:
310 channel.add_control_group(outputchannel)
312 def read_meters(self):
313 for channel in self.channels:
315 self.main_mix.read_meter()
316 for channel in self.output_channels:
320 def add_output_channel(self, name, stereo, volume_cc, balance_cc, display_solo_buttons):
322 channel = output_channel(self, name, stereo)
323 channel.display_solo_buttons = display_solo_buttons
324 self.add_output_channel_precreated(channel)
327 err = gtk.MessageDialog(self.window,
328 gtk.DIALOG_MODAL | gtk.DIALOG_DESTROY_WITH_PARENT,
331 "Channel creation failed")
336 channel.channel.volume_midi_cc = int(volume_cc)
338 channel.channel.balance_midi_cc = int(balance_cc)
341 def add_output_channel_precreated(self, channel):
344 self.hbox_outputs.pack_start(frame, False)
346 # XXX: handle deletion of output channels
347 #channel_remove_menu_item = gtk.MenuItem(channel.channel_name)
348 #self.channel_remove_menu.append(channel_remove_menu_item)
349 #channel_remove_menu_item.connect("activate", self.on_remove_channel, channel, channel_remove_menu_item)
350 #self.channel_remove_menu_item.set_sensitive(True)
351 self.output_channels.append(channel)
353 # add group controls to the input channels
354 for inputchannel in self.channels:
355 inputchannel.add_control_group(channel)
357 def lash_check_events(self):
358 while lash.lash_get_pending_event_count(self.lash_client):
359 event = lash.lash_get_event(self.lash_client)
363 event_type = lash.lash_event_get_type(event)
364 if event_type == lash.LASH_Quit:
365 print "jack_mixer: LASH ordered quit."
368 elif event_type == lash.LASH_Save_File:
369 directory = lash.lash_event_get_string(event)
370 print "jack_mixer: LASH ordered to save data in directory %s" % directory
371 filename = directory + os.sep + "jack_mixer.xml"
372 f = file(filename, "w")
375 lash.lash_send_event(self.lash_client, event) # we crash with double free
376 elif event_type == lash.LASH_Restore_File:
377 directory = lash.lash_event_get_string(event)
378 print "jack_mixer: LASH ordered to restore data from directory %s" % directory
379 filename = directory + os.sep + "jack_mixer.xml"
380 f = file(filename, "r")
381 self.load_from_xml(f)
383 lash.lash_send_event(self.lash_client, event)
385 print "jack_mixer: Got unhandled LASH event, type " + str(event_type)
388 #lash.lash_event_destroy(event)
392 def save_to_xml(self, file):
393 #print "Saving to XML..."
394 b = xml_serialization()
399 def load_from_xml(self, file):
400 #print "Loading from XML..."
401 self.on_channels_clear(None)
402 self.unserialized_channels = []
403 b = xml_serialization()
406 s.unserialize(self, b)
407 for channel in self.unserialized_channels:
408 if isinstance(channel, input_channel):
409 self.add_channel_precreated(channel)
411 self.add_output_channel_precreated(channel)
412 del self.unserialized_channels
413 self.window.show_all()
415 def serialize(self, object_backend):
416 object_backend.add_property('geometry',
417 '%sx%s' % (self.window.allocation.width, self.window.allocation.height))
419 def unserialize_property(self, name, value):
420 if name == 'geometry':
421 width, height = value.split('x')
422 self.window.resize(int(width), int(height))
425 def unserialize_child(self, name):
426 if name == main_mix_serialization_name():
429 if name == input_channel_serialization_name():
430 channel = input_channel(self, "", True)
431 self.unserialized_channels.append(channel)
434 if name == output_channel_serialization_name():
435 channel = output_channel(self, "", True)
436 self.unserialized_channels.append(channel)
439 def serialization_get_childs(self):
440 '''Get child objects tha required and support serialization'''
441 childs = self.channels[:] + self.output_channels[:]
442 childs.append(self.main_mix)
445 def serialization_name(self):
452 self.window.show_all()
456 #f = file("/dev/stdout", "w")
461 print "Usage: %s [mixer_name]" % sys.argv[0]
464 if lash: # If LASH python bindings are available
465 # sys.argv is modified by this call
466 lash_client = lash.init(sys.argv, "jack_mixer", lash.LASH_Config_File)
470 parser = OptionParser()
471 parser.add_option('-c', '--config', dest='config')
472 options, args = parser.parse_args()
474 # Yeah , this sounds stupid, we connected earlier, but we dont want to show this if we got --help option
475 # This issue should be fixed in pylash, there is a reason for having two functions for initialization after all
477 print "Successfully connected to LASH server at " + lash.lash_get_server_name(lash_client)
485 name = "jack_mixer-%u" % os.getpid()
487 gtk.gdk.threads_init()
489 mixer = jack_mixer(name, lash_client)
491 err = gtk.MessageDialog(None,
495 "Mixer creation failed (%s)" % str(e))
501 f = file(options.config)
502 mixer.current_filename = options.config
503 mixer.load_from_xml(f)
504 mixer.window.set_default_size(60*(1+len(mixer.channels)+len(mixer.output_channels)),300)
511 if __name__ == "__main__":