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
, 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
:
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
, 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
:
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
):
418 def unserialize_property(self
, name
, value
):
421 def unserialize_child(self
, name
):
422 if name
== main_mix_serialization_name():
425 if name
== input_channel_serialization_name():
426 channel
= input_channel(self
, "", True)
427 self
.unserialized_channels
.append(channel
)
430 if name
== output_channel_serialization_name():
431 channel
= output_channel(self
, "", True)
432 self
.unserialized_channels
.append(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
)
441 def serialization_name(self
):
448 self
.window
.show_all()
452 #f = file("/dev/stdout", "w")
457 print "Usage: %s [mixer_name]" % sys
.argv
[0]
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
)
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
473 print "Successfully connected to LASH server at " + lash
.lash_get_server_name(lash_client
)
481 name
= "jack_mixer-%u" % os
.getpid()
483 gtk
.gdk
.threads_init()
485 mixer
= jack_mixer(name
, lash_client
)
487 err
= gtk
.MessageDialog(None,
491 "Mixer creation failed (%s)" % str(e
))
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)
507 if __name__
== "__main__":