Pass height/width to cairo.Surface.create_similar as integers
[jack_mixer.git] / jack_mixer.py
blob4985bb394f7f8a5940bd61c3a036951cf1219865
1 #!/usr/bin/env python
3 # This file is part of jack_mixer
5 # Copyright (C) 2006 Nedko Arnaudov <nedko@arnaudov.name>
6 #
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
22 import gtk
23 import gobject
24 import jack_mixer_c
25 import sys
26 import os
28 try:
29 import lash
30 except:
31 lash = None
33 old_path = sys.path
34 sys.path.insert(0, os.path.dirname(sys.argv[0]) + os.sep + ".." + os.sep + "share"+ os.sep + "jack_mixer")
35 from channel import *
36 import gui
37 from preferences import PreferencesDialog
39 sys.path = old_path
41 try:
42 from serialization_xml import xml_serialization
43 from serialization import serialized_object, serializator
44 except ImportError:
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)
63 if not self.mixer:
64 return
66 if lash_client:
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)
152 self.channels = []
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)
163 frame = gtk.Frame()
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
173 if lash_client:
174 gobject.timeout_add(1000, self.lash_check_events)
176 def cleanup(self):
177 print "Cleaning jack_mixer"
178 if not self.mixer:
179 return
181 for channel in self.channels:
182 channel.unrealize()
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()
192 try:
193 f = file(filename, 'r')
194 self.load_from_xml(f)
195 except:
196 # TODO: display error in a dialog box
197 print >> sys.stderr, 'Failed to read', filename
198 else:
199 self.current_filename = filename
200 finally:
201 f.close()
202 dlg.destroy()
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')
208 self.save_to_xml(f)
209 f.close()
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()
219 self.on_save_cb()
220 dlg.destroy()
222 def on_quit_cb(self, *args):
223 gtk.main_quit()
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)
235 dialog.show()
236 ret = dialog.run()
237 dialog.hide()
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)
247 dialog.show()
248 ret = dialog.run()
249 dialog.hide()
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, channel_remove_menu_item):
257 print 'Removing channel "%s"' % channel.channel_name
258 self.channel_remove_menu.remove(channel_remove_menu_item)
259 for i in range(len(self.channels)):
260 if self.channels[i] is channel:
261 channel.unrealize()
262 del self.channels[i]
263 self.hbox_inputs.remove(channel.parent)
264 break
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:
270 channel.unrealize()
271 self.hbox_inputs.remove(channel.parent)
272 self.channels = []
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):
278 try:
279 channel = input_channel(self, name, stereo)
280 self.add_channel_precreated(channel)
281 except Exception:
282 err = gtk.MessageDialog(self.window,
283 gtk.DIALOG_MODAL | gtk.DIALOG_DESTROY_WITH_PARENT,
284 gtk.MESSAGE_ERROR,
285 gtk.BUTTONS_OK,
286 "Channel creation failed")
287 err.run()
288 err.destroy()
289 return
290 if volume_cc:
291 channel.channel.volume_midi_cc = int(volume_cc)
292 if balance_cc:
293 channel.channel.balance_midi_cc = int(balance_cc)
294 if not (volume_cc or balance_cc):
295 channel.channel.autoset_midi_cc()
296 return channel
298 def add_channel_precreated(self, channel):
299 frame = gtk.Frame()
300 frame.add(channel)
301 self.hbox_inputs.pack_start(frame, False)
302 channel.realize()
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, channel_remove_menu_item)
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:
314 channel.read_meter()
315 self.main_mix.read_meter()
316 for channel in self.output_channels:
317 channel.read_meter()
318 return True
320 def add_output_channel(self, name, stereo, volume_cc, balance_cc, display_solo_buttons):
321 try:
322 channel = output_channel(self, name, stereo)
323 channel.display_solo_buttons = display_solo_buttons
324 self.add_output_channel_precreated(channel)
325 except Exception:
326 raise
327 err = gtk.MessageDialog(self.window,
328 gtk.DIALOG_MODAL | gtk.DIALOG_DESTROY_WITH_PARENT,
329 gtk.MESSAGE_ERROR,
330 gtk.BUTTONS_OK,
331 "Channel creation failed")
332 err.run()
333 err.destroy()
334 return
335 if volume_cc:
336 channel.channel.volume_midi_cc = int(volume_cc)
337 if balance_cc:
338 channel.channel.balance_midi_cc = int(balance_cc)
339 return channel
341 def add_output_channel_precreated(self, channel):
342 frame = gtk.Frame()
343 frame.add(channel)
344 self.hbox_outputs.pack_start(frame, False)
345 channel.realize()
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)
361 #print repr(event)
363 event_type = lash.lash_event_get_type(event)
364 if event_type == lash.LASH_Quit:
365 print "jack_mixer: LASH ordered quit."
366 gtk.main_quit()
367 return False
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")
373 self.save_to_xml(f)
374 f.close()
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)
382 f.close()
383 lash.lash_send_event(self.lash_client, event)
384 else:
385 print "jack_mixer: Got unhandled LASH event, type " + str(event_type)
386 return True
388 #lash.lash_event_destroy(event)
390 return True
392 def save_to_xml(self, file):
393 #print "Saving to XML..."
394 b = xml_serialization()
395 s = serializator()
396 s.serialize(self, b)
397 b.save(file)
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()
404 b.load(file)
405 s = serializator()
406 s.unserialize(self, b)
407 for channel in self.unserialized_channels:
408 if isinstance(channel, input_channel):
409 self.add_channel_precreated(channel)
410 else:
411 self.add_output_channel_precreated(channel)
412 del self.unserialized_channels
413 self.window.show_all()
415 def serialize(self, object_backend):
416 pass
418 def unserialize_property(self, name, value):
419 pass
421 def unserialize_child(self, name):
422 if name == main_mix_serialization_name():
423 return self.main_mix
425 if name == input_channel_serialization_name():
426 channel = input_channel(self, "", True)
427 self.unserialized_channels.append(channel)
428 return channel
430 if name == output_channel_serialization_name():
431 channel = output_channel(self, "", True)
432 self.unserialized_channels.append(channel)
433 return channel
435 def serialization_get_childs(self):
436 '''Get child objects tha required and support serialization'''
437 childs = self.channels[:] + self.output_channels[:]
438 childs.append(self.main_mix)
439 return childs
441 def serialization_name(self):
442 return "jack_mixer"
444 def main(self):
445 if not self.mixer:
446 return
448 self.window.show_all()
450 gtk.main()
452 #f = file("/dev/stdout", "w")
453 #self.save_to_xml(f)
454 #f.close
456 def help():
457 print "Usage: %s [mixer_name]" % sys.argv[0]
459 def main():
460 if lash: # If LASH python bindings are available
461 # sys.argv is modified by this call
462 lash_client = lash.init(sys.argv, "jack_mixer", lash.LASH_Config_File)
463 else:
464 lash_client = None
466 parser = OptionParser()
467 parser.add_option('-c', '--config', dest='config')
468 options, args = parser.parse_args()
470 # Yeah , this sounds stupid, we connected earlier, but we dont want to show this if we got --help option
471 # This issue should be fixed in pylash, there is a reason for having two functions for initialization after all
472 if lash_client:
473 print "Successfully connected to LASH server at " + lash.lash_get_server_name(lash_client)
475 if len(args) == 1:
476 name = args[0]
477 else:
478 name = None
480 if not name:
481 name = "jack_mixer-%u" % os.getpid()
483 gtk.gdk.threads_init()
484 try:
485 mixer = jack_mixer(name, lash_client)
486 except Exception, e:
487 err = gtk.MessageDialog(None,
488 gtk.DIALOG_MODAL,
489 gtk.MESSAGE_ERROR,
490 gtk.BUTTONS_OK,
491 "Mixer creation failed (%s)" % str(e))
492 err.run()
493 err.destroy()
494 sys.exit(1)
496 if options.config:
497 f = file(options.config)
498 mixer.current_filename = options.config
499 mixer.load_from_xml(f)
500 mixer.window.set_default_size(60*(1+len(mixer.channels)+len(mixer.output_channels)),300)
501 f.close()
503 mixer.main()
505 mixer.cleanup()
507 if __name__ == "__main__":
508 main()