Ejemplo n.º 1
0
class PulseCasterUI(Gtk.Application):
    def __init__(self):
        Gtk.Application.__init__(
            self,
            application_id='apps.org.pulsecaster.PulseCaster',
            flags=Gio.ApplicationFlags.FLAGS_NONE)
        self.connect('activate', self.on_activate)

    def on_activate(self, app):
        self.builder = Gtk.Builder()
        self.builder.set_translation_domain(NAME)
        try:
            self.builder.add_from_file(
                os.path.join(os.getcwd(), 'data', 'pulsecaster.ui'))
            debugPrint(_("loading UI file from current subdir"))
        except:
            try:
                self.builder.add_from_file(
                    os.path.join(sys.prefix, 'share', 'pulsecaster',
                                 'pulsecaster.ui'))
            except:
                try:
                    self.builder.add_from_file(
                        os.path.join(os.path.dirname(sys.argv[0]), 'data',
                                     'pulsecaster.ui'))
                except Exception as e:
                    print(e)
                    raise SystemExit(_("Cannot load resources"))

        self.tempgsettings = Gtk.Settings.get_default()
        self.tempgsettings.set_property('gtk-application-prefer-dark-theme',
                                        True)
        self.icontheme = Gtk.IconTheme.get_default()
        # Convenience for developers
        self.icontheme.append_search_path(
            os.path.join(os.getcwd(), 'data', 'icons', 'scalable'))
        self.icontheme.append_search_path(
            os.path.join(os.path.dirname(sys.argv[0]), 'data', 'icons',
                         'scalable'))
        self.logo = self.icontheme.load_icon('pulsecaster', -1,
                                             Gtk.IconLookupFlags.FORCE_SVG)
        Gtk.Window.set_default_icon(self.logo)
        self.gsettings = PulseCasterGSettings()

        self.warning = self.builder.get_object('warning')
        self.add_window(self.warning)
        self.dismiss = self.builder.get_object('dismiss_warning')
        self.swckbox = self.builder.get_object('skip_warn_checkbox')
        self.swckbox.set_active(int(self.gsettings.skip_warn))
        self.dismiss.connect('clicked', self.hideWarn)
        self.warning.connect('destroy', self.on_close)
        self.warning.set_title(NAME)

        # Miscellaneous dialog strings
        s = _('Important notice')
        self.builder.get_object('warning-label2').set_label('<big><big>' +
                                                            '<b><i>' + s +
                                                            '</i></b>' +
                                                            '</big></big>')
        s = _('This program can be used to record speech from remote ' +
              'locations. You are responsible for adhering to all ' +
              'applicable laws and regulations when using this program. ' +
              'In general you should not record other parties without ' +
              'their consent.')
        self.builder.get_object('warning-label3').set_label(s)
        s = _('Do not show this again')
        self.builder.get_object('skip_warn_checkbox').set_label(s)
        s = _('Select the audio sources to mix')
        self.builder.get_object('label2').set_label(s)
        s = _('I understand.')
        self.builder.get_object('dismiss_warning').set_label(s)
        s = _('Your voice')
        self.builder.get_object('label3').set_label(s + ':')
        s = _('Subject\'s voice')
        self.builder.get_object('label4').set_label(s + ':')

        # Main dialog basics
        self.main = self.builder.get_object('main_dialog')
        self.add_window(self.main)
        self.main.set_title(NAME)
        self.main_title = self.builder.get_object('main_title')
        self.main_title.set_label('<big><big><big><b><i>' + NAME +
                                  '</i></b></big></big></big>')
        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.adv_button = self.builder.get_object('adv_button')
        self.adv_button.connect('clicked', self.showAdv)
        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.IconSize.DIALOG)
        self.main.set_icon_list([self.logo])
        # Advanced dialog basics
        self.adv = self.builder.get_object('adv_dialog')
        self.adv.set_icon_list([self.logo])
        self.adv.set_title(NAME)
        self.adv.connect('delete_event', self.hideAdv)
        self.adv.connect('response', self.hideAdv)
        self.adv_stdlabel1 = self.builder.get_object('adv_stdlabel1')
        self.adv_stdlabel2 = self.builder.get_object('adv_stdlabel2')
        self.adv_explabel1 = self.builder.get_object('adv_explabel1')
        self.adv_explabel2 = self.builder.get_object('adv_explabel2')
        self.adv_stdlabel1.set_label(_('Standard settings'))
        self.adv_explabel1.set_label(_('Expert settings'))
        lbl = _('Save the conversation as a single audio file with ' +
                'compression. This is the right option for most people.')
        self.adv_stdlabel2.set_label('<small><i>' + lbl + '</i></small>')
        lbl = _('Save each voice as a separate audio file without ' +
                'compression. Use this option to mix and encode audio ' +
                'yourself.')
        self.adv_explabel2.set_label('<small><i>' + lbl + '</i></small>')
        # TODO: Add bits to set radio buttons and make them work
        self.vorbis_button = self.builder.get_object('vorbis_button')
        self.vorbis_button.connect('clicked', self.set_standard)
        self.flac_button = self.builder.get_object('flac_button')
        self.flac_button.connect('clicked', self.set_expert)
        self.flac_button.join_group(self.vorbis_button)
        if self.gsettings.expert is True:
            self.flac_button.set_active(True)
        else:
            self.vorbis_button.set_active(True)
        # About dialog basics
        self.about = self.builder.get_object('about_dialog')
        self.add_window(self.about)
        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.IconLookupFlags.FORCE_SVG))

        # Create PulseAudio backing
        self.pa = Pulse(client_name=NAME)

        # Create and populate combo boxes
        self.table = self.builder.get_object('table1')
        self.user_vox = PulseCasterSource()
        self.subject_vox = PulseCasterSource()

        self.table.attach(self.user_vox.cbox, 1, 0, 1, 1)
        self.table.attach(self.subject_vox.cbox, 1, 1, 1, 1)
        self.table.attach(self.user_vox.pbar, 2, 0, 1, 1)
        self.table.attach(self.subject_vox.pbar, 2, 1, 1, 1)
        self.user_vox.cbox.connect('button-press-event',
                                   self.user_vox.repopulate, self.pa)
        self.subject_vox.cbox.connect('button-press-event',
                                      self.subject_vox.repopulate, self.pa)

        # Fill the combo boxes initially
        self.repop_sources()
        self.user_vox.cbox.set_active(0)
        self.subject_vox.cbox.set_active(0)
        self.table.show_all()

        self.listener = PulseCasterListener(self)
        self.filesinkpath = ''

        if self.gsettings.skip_warn is False:
            self.warning.show()
        else:
            self.hideWarn()

    def repop_sources(self, *args):
        self.main.set_sensitive(False)
        self.user_vox.repopulate(self.pa, use_source=True, use_monitor=False)
        self.subject_vox.repopulate(self.pa,
                                    use_source=False,
                                    use_monitor=True)
        self.table.show_all()
        self.main.set_sensitive(True)

    def on_record(self, *args):
        # Adjust UI
        self.user_vox.cbox.set_sensitive(False)
        self.subject_vox.cbox.set_sensitive(False)
        self.close.set_sensitive(False)
        self.adv_button.set_sensitive(False)

        self.combiner = Gst.Pipeline()
        self.lsource = Gst.ElementFactory.make('pulsesrc', 'lsrc')
        self.lsource.set_property('device', self.user_vox.pulsesrc)
        self.rsource = Gst.ElementFactory.make('pulsesrc', 'rsrc')
        self.rsource.set_property('device', self.subject_vox.pulsesrc)

        self._default_caps = Gst.Caps.from_string('audio/x-raw, '
                                                  'rate=(int)%d' %
                                                  (self.gsettings.audiorate))
        self.adder = Gst.ElementFactory.make('adder', 'mix')
        self.lfilter = Gst.ElementFactory.make('capsfilter', 'lfilter')
        self.rfilter = Gst.ElementFactory.make('capsfilter', 'rfilter')
        debugPrint('audiorate: %d' % self.gsettings.audiorate)

        if self.gsettings.expert is not True:
            # Create temporary file
            (self.tempfd,
             self.temppath) = tempfile.mkstemp(prefix='%s-tmp.' % (NAME))
            self.tempfile = os.fdopen(self.tempfd)
            debugPrint('tempfile: %s (fd %s)' % (self.temppath, self.tempfd))
            self.encoder = Gst.ElementFactory.make(
                self.gsettings.codec + 'enc', 'enc')
            if self.gsettings.codec == 'vorbis':
                self.muxer = Gst.ElementFactory.make('oggmux', 'mux')
            self.filesink = Gst.ElementFactory.make('filesink', 'fsink')
            self.filesink.set_property('location', self.temppath)

            for e in (self.lsource, self.lfilter, self.rsource, self.rfilter,
                      self.adder, self.encoder, self.filesink):
                self.combiner.add(e)
            if self.gsettings.codec == 'vorbis':
                self.combiner.add(self.muxer)
            self.lsource.link(self.lfilter)
            self.lfilter.link(self.adder)
            self.adder.link(self.encoder)
            if self.gsettings.codec == 'vorbis':
                self.encoder.link(self.muxer)
                self.muxer.link(self.filesink)
            else:  # flac
                self.encoder.link(self.filesink)
            self.rsource.link(self.rfilter)
            self.rfilter.link(self.adder)
        else:
            # Create temporary file
            (self.tempfd1,
             self.temppath1) = tempfile.mkstemp(prefix='%s-1-tmp.' % (NAME))
            (self.tempfd2,
             self.temppath2) = tempfile.mkstemp(prefix='%s-2-tmp.' % (NAME))
            self.tempfile1 = os.fdopen(self.tempfd1)
            self.tempfile2 = os.fdopen(self.tempfd2)
            debugPrint(
                'tempfiles: %s (fd %s), %s (fd %s)' %
                (self.temppath1, self.tempfd1, self.temppath2, self.temppath2))
            # We're in expert mode
            # Disregard vorbis, use WAV
            self.encoder1 = Gst.ElementFactory.make('wavenc', 'enc1')
            self.encoder2 = Gst.ElementFactory.make('wavenc', 'enc2')
            self.filesink1 = Gst.ElementFactory.make('filesink', 'fsink1')
            self.filesink1.set_property('location', self.temppath1)
            self.filesink2 = Gst.ElementFactory.make('filesink', 'fsink2')
            self.filesink2.set_property('location', self.temppath2)
            for e in (self.lsource, self.lfilter, self.rsource, self.rfilter,
                      self.encoder1, self.encoder2, self.filesink1,
                      self.filesink2):
                self.combiner.add(e)
            self.lsource.link(self.lfilter)
            self.lfilter.link(self.encoder1)
            self.encoder1.link(self.filesink1)
            self.rsource.link(self.rfilter)
            self.rfilter.link(self.encoder2)
            self.encoder2.link(self.filesink2)

        # 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)

    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.cbox.set_sensitive(True)
        self.subject_vox.cbox.set_sensitive(True)
        self.close.set_sensitive(True)
        self.adv_button.set_sensitive(True)
        self.record.show()

    def on_close(self, *args):
        try:
            self.pa.disconnect()
        except:
            pass
        self.quit()

    def hideWarn(self, *args):
        self.gsettings.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 showAdv(self, *args):
        if self.gsettings.expert is True:
            self.flac_button.set_active(True)
        else:
            self.vorbis_button.set_active(True)
        self.adv.show()

    def hideAdv(self, *args):
        self.adv.hide()

    def set_standard(self, *args):
        self.gsettings.gsettings.set_boolean('expert', False)
        self.gsettings.gsettings.set_string('codec', 'vorbis')
        self.gsettings.expert = False
        self.gsettings.gsettings.sync()

    def set_expert(self, *args):
        self.gsettings.gsettings.set_boolean('expert', True)
        self.gsettings.gsettings.set_string('codec', 'flac')
        self.gsettings.expert = True
        self.gsettings.gsettings.sync()

    def showFileChooser(self, *args):
        self.file_chooser = Gtk.FileChooserDialog(
            title=_('Save your recording'),
            action=Gtk.FileChooserAction.SAVE,
            buttons=(Gtk.STOCK_CANCEL, Gtk.ResponseType.CANCEL, Gtk.STOCK_OK,
                     Gtk.ResponseType.OK))
        self.file_chooser.set_local_only(True)
        response = self.file_chooser.run()
        if response == Gtk.ResponseType.OK:
            self.updateFileSinkPath()
        elif response == Gtk.ResponseType.CANCEL:
            self.hideFileChooser()
        elif response == Gtk.ResponseType.DELETE_EVENT:
            self.hideFileChooser()

    def hideFileChooser(self, *args):
        if not self.filesinkpath:
            confirm_message = _('Are you sure you want to cancel saving ' +
                                'your work? If you choose Yes your audio ' +
                                'recording will be erased permanently.')
            confirm = Gtk.MessageDialog(type=Gtk.MessageType.WARNING,
                                        buttons=Gtk.ButtonsType.YES_NO,
                                        message_format=confirm_message)
            response = confirm.run()
            confirm.destroy()
            if response == Gtk.ResponseType.YES:
                if self.gsettings.expert is False:
                    self._remove_tempfile(self.tempfile, self.temppath)
                else:
                    self._remove_tempfile(self.tempfile1, self.temppath1)
                    self._remove_tempfile(self.tempfile2, self.temppath2)
            else:
                return
        self.file_chooser.destroy()

    def updateFileSinkPath(self, *args):
        self.filesinkpath = self.file_chooser.get_filename()
        if not self.filesinkpath:
            return
        self.hideFileChooser()
        if self.gsettings.expert is False:
            if not self.filesinkpath.endswith('.ogg'):
                self.filesinkpath += '.ogg'
            if os.path.lexists(self.filesinkpath):
                if not self._confirm_overwrite():
                    self.showFileChooser()
                    return
        else:
            if os.path.lexists(self.filesinkpath+'-1.wav') or \
                    os.path.lexists(self.filesinkpath+'-2.wav'):
                if not self._confirm_overwrite():
                    self.showFileChooser()
                    return
        # Copy the temporary file to its new home
        self._copy_temp_to_perm()
        if self.gsettings.expert is False:
            self._remove_tempfile(self.tempfile, self.temppath)
        else:
            expert_message = _('WAV files are written here:')
            expert_message += '\n%s\n%s' % (self.filesinkpath + '-1.wav',
                                            self.filesinkpath + '-2.wav')
            expertdlg = Gtk.MessageDialog(type=Gtk.MessageType.INFO,
                                          buttons=Gtk.ButtonsType.OK,
                                          message_format=expert_message)
            response = expertdlg.run()
            expertdlg.destroy()
            self._remove_tempfile(self.tempfile1, self.temppath1)
            self._remove_tempfile(self.tempfile2, self.temppath2)
        self.record.set_sensitive(True)

    def _update_time(self, *args):
        if self.combiner.get_state(Gst.CLOCK_TIME_NONE)[1] == Gst.State.NULL:
            return False
        delta = datetime.now() - self.starttime
        deltamin = delta.seconds // 60
        deltasec = delta.seconds - (deltamin * 60)
        return True

    def _confirm_overwrite(self, *args):
        confirm_message = _('File exists. OK to overwrite?')
        confirm = Gtk.MessageDialog(type=Gtk.MessageType.QUESTION,
                                    buttons=Gtk.ButtonsType.YES_NO,
                                    message_format=confirm_message)
        response = confirm.run()
        if response == Gtk.ResponseType.YES:
            retval = True
        else:
            retval = False
        confirm.destroy()
        return retval

    def _copy_temp_to_perm(self):
        # This is a really stupid way to do this.
        # FIXME: abstract out the duplicated code, lazybones.
        if self.gsettings.expert is False:
            permfile = open(self.filesinkpath, 'wb')
            self.tempfile.close()
            self.tempfile = open(self.temppath, 'rb')
            permfile.write(self.tempfile.read())
            permfile.close()
        else:
            for i in (1, 2):
                permfile = open(self.filesinkpath + '-' + str(i) + '.wav',
                                'wb')
                tf = eval('self.tempfile' + str(i))
                tf.close()
                tempfile = open(eval('self.temppath' + str(i)), 'rb')
                permfile.write(tempfile.read())
                permfile.close()

    def _remove_tempfile(self, tempfile, temppath):
        tempfile.close()
        os.remove(temppath)
Ejemplo n.º 2
0
class Py3status:
    py3: Py3
    blocks = u"_▁▂▃▄▅▆▇█"
    button_down = 5
    button_mute = 1
    button_up = 4
    format = u"{icon} {percentage}%"
    format_muted = u"{icon} {percentage}%"
    is_input = False
    max_volume = 100
    thresholds = [(0, "good"), (75, "degraded"), (100, "bad")]
    volume_delta = 5

    def __init__(self,
                 sink_name: Optional[str] = None,
                 volume_boost: bool = False):
        """

        :param sink_name:  Sink name to use. Empty uses default sink
        :param volume_boost: Whether to allow setting volume above 1.0 - uses software boost
        """
        self._sink_name = sink_name
        self._sink_info: Optional[PulseSinkInfo]
        self._volume_boost = volume_boost
        self._pulse_connector = Pulse('py3status-pulse-connector',
                                      threading_lock=True)
        self._pulse_connector_lock = threading.Lock()
        self._volume: Optional[Volume] = None
        self._backend_thread = threading.Thread

    def _get_volume_from_backend(self):
        """Get a new sink on every call.

        The sink is not updated when the backed values change.
        Returned volume is the maximum of all available channels.
        """
        sink_name = self._pulse_connector.server_info().default_sink_name
        self._sink_info = self._pulse_connector.get_sink_by_name(sink_name)
        pulse_volume = Volume.from_sink_info(self._sink_info)
        logger.debug(pulse_volume)
        if self._volume != pulse_volume:
            self._volume = pulse_volume
            self.py3.update()

    def _callback(self, ev):
        if ev.t == PulseEventTypeEnum.change and \
                (ev.facility == PulseEventFacilityEnum.server or
                 ev.facility == PulseEventFacilityEnum.sink and ev.index == self._sink_info.index):
            raise PulseLoopStop

    def _pulse_reader(self):
        while True:
            try:
                self._pulse_connector.event_listen()
                self._get_volume_from_backend()
            except PulseDisconnected:
                logger.debug("Pulse disconnected. Stopping reader.")
                break

    def post_config_hook(self):
        self._pulse_connector.connect()
        self._get_volume_from_backend()
        self._pulse_connector.event_mask_set(PulseEventMaskEnum.server,
                                             PulseEventMaskEnum.sink)
        self._pulse_connector.event_callback_set(self._callback)
        self._backend_thread = threading.Thread(
            name="pulse_backend", target=self._pulse_reader).start()

    def kill(self):
        logger.info("Shutting down")
        self._pulse_connector.disconnect()

    def _color_for_output(self) -> str:
        if self._volume is None:
            return self.py3.COLOR_BAD
        if self._volume.mute:
            return self.py3.COLOR_MUTED or self.py3.COLOR_BAD
        return self.py3.threshold_get_color(self._volume.level)

    def _icon_for_output(self) -> str:
        return self.blocks[min(
            len(self.blocks) - 1,
            int(math.ceil(self._volume.level / 100 * (len(self.blocks) - 1))),
        )]

    def _format_output(self) -> Union[str, Composite]:
        return self.py3.safe_format(format_string=self.format_muted
                                    if self._volume.mute else self.format,
                                    param_dict={
                                        "icon": self._icon_for_output(),
                                        "percentage": self._volume.level
                                    })

    def volume_status(self):
        response = {
            "cached_until": self.py3.CACHE_FOREVER,
            "color": self._color_for_output(),
            "full_text": self._format_output()
        }
        return response