# Copyright (C) 2009, 2010 Paul W. Frields and others. # -*- coding: utf-8 -*- # # This program is free software: you can redistribute it and/or modify # it under the terms of the GNU General Public License as published by # the Free Software Foundation, either version 3 of the License, or # (at your option) any later version. # # This program is distributed in the hope that it will be useful, # but WITHOUT ANY WARRANTY; without even the implied warranty of # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the # GNU General Public License for more details. # # You should have received a copy of the GNU General Public License # along with this program. If not, see . # # # Author: Paul W. Frields # Jürgen Geuter from config import * import gconfig from pulseaudio.PulseObj import PulseObj from listener import * from eggtrayicon import * import gtk import os import sys import tempfile import gobject import pygst pygst.require('0.10') import gst from datetime import datetime import gettext _ = lambda x: gettext.ldgettext(NAME, x) try: _debug = os.environ['PULSECASTER_DEBUG'] except: _debug = False def _debugPrint(text): if _debug: print (text) class PulseCasterUI: def __init__(self): self.builder = gtk.Builder() try: self.builder.add_from_file(os.path.join(os.getcwd(),'data','pulsecaster.glade') ) _debugPrint(_("loading glade file from current subdir")) except: try: self.builder.add_from_file(os.path.join(sys.prefix,'share','pulsecaster','pulsecaster.glade')) except Exception,e: print(e) raise SystemExit(_("Cannot load resources")) self.icontheme = gtk.icon_theme_get_default() # Convenience for developers self.icontheme.append_search_path(os.path.join(os.getcwd(),'data','icons','scalable')) self.logo = self.icontheme.load_icon('pulsecaster', -1, gtk.ICON_LOOKUP_FORCE_SVG) gtk.window_set_default_icon(self.logo) self.gconfig = gconfig.PulseCasterGconf() self.warning = self.builder.get_object('warning') self.dismiss = self.builder.get_object('dismiss_warning') self.swckbox = self.builder.get_object('skip_warn_checkbox') self.swckbox.set_active(int(self.gconfig.skip_warn)) self.dismiss.connect('clicked', self.hideWarn) self.warning.connect('destroy', self.on_close) self.warning.set_title(NAME) # Main dialog basics self.main = self.builder.get_object('main_dialog') self.main.set_title(NAME) self.main_title = self.builder.get_object('main_title') self.main_title.set_label('' + NAME + '') self.main.connect('delete_event', self.on_close) self.about_button = self.builder.get_object('about_button') self.about_button.connect('clicked', self.showAbout) self.close = self.builder.get_object('close_button') self.close.connect('clicked', self.on_close) self.record = self.builder.get_object('record_button') self.record_id = self.record.connect('clicked', self.on_record) self.record.set_sensitive(True) self.main_logo = self.builder.get_object('logo') self.main_logo.set_from_icon_name('pulsecaster', gtk.ICON_SIZE_DIALOG) self.main.set_icon_list(self.logo) # About dialog basics self.about = self.builder.get_object('about_dialog') self.about.connect('delete_event', self.hideAbout) self.about.connect('response', self.hideAbout) self.about.set_name(NAME) self.about.set_version(VERSION) self.about.set_copyright(COPYRIGHT) self.about.set_comments(DESCRIPTION) self.about.set_license(LICENSE_TEXT) self.about.set_website(URL) self.about.set_website_label(URL) self.authors = [AUTHOR + ' <' + AUTHOR_EMAIL + '>'] for contrib in CONTRIBUTORS: self.authors.append(contrib) self.about.set_authors(self.authors) self.about.set_program_name(NAME) self.about.set_logo(self.icontheme.load_icon('pulsecaster', 96, gtk.ICON_LOOKUP_FORCE_SVG)) self.file_chooser = self.builder.get_object('file_chooser') self.file_chooser_cancel_button = self.builder.get_object('file_chooser_cancel_button') self.file_chooser_cancel_button.connect('clicked', self.hideFileChooser) self.file_chooser_save_button = self.builder.get_object('file_chooser_save_button') self.file_chooser_save_button.connect('clicked', self.updateFileSinkPath) self.file_chooser.set_do_overwrite_confirmation(True) self.file_chooser.connect('confirm-overwrite', self._confirm_overwrite) # Create PulseAudio backing self.pa = PulseObj(clientName=NAME) # Create and populate combo boxes self.table = self.builder.get_object('table1') self.user_vox = gtk.combo_box_new_text() self.subject_vox = gtk.combo_box_new_text() self.table.attach(self.user_vox, 1, 2, 0, 1, xoptions=gtk.EXPAND|gtk.FILL) self.table.attach(self.subject_vox, 1, 2, 1, 2, xoptions=gtk.EXPAND|gtk.FILL) self.user_vox.connect('button-press-event', self.repop_sources) self.subject_vox.connect('button-press-event', self.repop_sources) # Fill the combo boxes initially self.repop_sources() self.listener = PulseCasterListener(self) self.filesinkpath = '' self.trayicon = gtk.StatusIcon() self.trayicon.set_visible(False) self.trayicon.set_from_icon_name('pulsecaster') def repop_sources(self, *args): self.sources = self.pa.pulse_source_list() l = self.user_vox.get_model() l.clear() l = self.subject_vox.get_model() l.clear() self.uservoxes = [] self.subjectvoxes = [] for source in self.sources: if source.monitor_of_sink_name == None: self.uservoxes.append((source.name, source.description)) self.user_vox.append_text(source.description) else: self.subjectvoxes.append((source.name, source.description)) self.subject_vox.append_text(source.description) self.user_vox.set_active(0) self.subject_vox.set_active(0) self.table.show_all() if self.gconfig.skip_warn is False: self.warning.show() else: self.hideWarn() def on_record(self, *args): # Create temporary file (self.tempfd, self.temppath) = tempfile.mkstemp(prefix='%s-tmp.' % (NAME)) self.tempfile = os.fdopen(self.tempfd) _debugPrint('%s (%s)' % (self.temppath, self.tempfd)) # Adjust UI self.user_vox.set_sensitive(False) self.subject_vox.set_sensitive(False) self.close.set_sensitive(False) self.combiner = gst.Pipeline('PulseCasterCombinePipe') self.lsource = gst.element_factory_make('pulsesrc', 'lsrc') self.lsource.set_property('device', self.uservoxes[self.user_vox.get_active()][0]) self.rsource = gst.element_factory_make('pulsesrc', 'rsrc') self.rsource.set_property('device', self.subjectvoxes[self.subject_vox.get_active()][0]) self.adder = gst.element_factory_make('adder', 'mix') self.encoder = gst.element_factory_make(self.gconfig.codec + 'enc', 'enc') if self.gconfig.codec == 'vorbis': self.muxer = gst.element_factory_make('oggmux', 'mux') self.filesink = gst.element_factory_make('filesink', 'fsink') self.filesink.set_property('location', self.temppath) self.combiner.add(self.lsource, self.rsource, self.adder, self.encoder, self.filesink) if self.gconfig.codec == 'vorbis': self.combiner.add(self.muxer) gst.element_link_many(self.lsource, self.adder, self.encoder) if self.gconfig.codec == 'vorbis': self.encoder.link(self.muxer) self.muxer.link(self.filesink) else: # flac self.encoder.link(self.filesink) gst.element_link_many(self.rsource, self.adder) # FIXME: Dim elements other than the 'record' widget self.record.set_label(gtk.STOCK_MEDIA_STOP) self.record.disconnect(self.record_id) self.stop_id = self.record.connect('clicked', self.on_stop) self.record.show() self.combiner.set_state(gst.STATE_PLAYING) # Start timer self.starttime = datetime.now() self._update_time() self.timeout = 1000 gobject.timeout_add(self.timeout, self._update_time) self.trayicon.set_visible(True) def on_stop(self, *args): self.combiner.set_state(gst.STATE_NULL) self.showFileChooser() self.record.set_label(gtk.STOCK_MEDIA_RECORD) self.record.disconnect(self.stop_id) self.record_id = self.record.connect('clicked', self.on_record) self.user_vox.set_sensitive(True) self.subject_vox.set_sensitive(True) self.close.set_sensitive(True) self.record.show() def on_close(self, *args): try: self.pa.disconnect() except: pass gtk.main_quit() def hideWarn(self, *args): self.gconfig.change_warn(self.swckbox.get_active()) self.warning.hide() self.main.show() def showAbout(self, *args): self.about.show() def hideAbout(self, *args): self.about.hide() def showFileChooser(self, *args): self.file_chooser.show() def hideFileChooser(self, *args): if not self.filesinkpath: confirm = gtk.MessageDialog(type=gtk.MESSAGE_WARNING, buttons=gtk.BUTTONS_YES_NO, message_format=_('Are you sure you want to cancel ' + 'saving your work? If you choose "Yes" ' + 'your audio recording will be erased ' + 'permanently.')) response = confirm.run() confirm.destroy() if response == gtk.RESPONSE_YES: self._remove_tempfile(self.tempfile, self.temppath) else: return self.file_chooser.hide() def updateFileSinkPath(self, *args): self.filesinkpath = self.file_chooser.get_filename() if not self.filesinkpath: return self.hideFileChooser() if os.path.lexists(self.filesinkpath): if not self._confirm_overwrite(): self.showFileChooser() return # Copy the temporary file to its new home self.permfile = open(self.filesinkpath, 'w') self._copy_temp_to_perm(self.tempfile, self.permfile) self.permfile.close() self._remove_tempfile(self.tempfile, self.temppath) self.record.set_sensitive(True) def _update_time(self, *args): if self.combiner.get_state()[1] == gst.STATE_NULL: self.trayicon.set_tooltip(None) self.trayicon.set_visible(False) return False delta = datetime.now() - self.starttime deltamin = delta.seconds // 60 deltasec = delta.seconds - (deltamin * 60) self.trayicon.set_tooltip('Recording: %d:%02d' % (deltamin, deltasec)) return True def _confirm_overwrite(self, *args): confirm = gtk.MessageDialog(type=gtk.MESSAGE_QUESTION, buttons=gtk.BUTTONS_YES_NO, message_format=_('File exists. OK to overwrite?')) response = confirm.run() if response == gtk.RESPONSE_YES: retval = True else: retval = False confirm.destroy() return retval def _copy_temp_to_perm(self, src, dest): src.seek(0) while True: buf = src.read(1024*1024) if buf: dest.write(buf) else: break def _remove_tempfile(self, tempfile, temppath): tempfile.close() os.remove(temppath) if __name__ == '__main__': pulseCaster = PulseCasterUI() gtk.main()