From d7c9645b60dd4e77794d2145d979dbacc9494c40 Mon Sep 17 00:00:00 2001 From: Gonzalo Odiard Date: Sun, 9 Nov 2014 21:54:34 -0300 Subject: [PATCH] Move SpeechManager from sugar to sugar-toolkit-gtk3 This is done to be able to use this implementation of the feature in the activities. A complementary patch is needed in sugar module. --- src/sugar3/Makefile.am | 1 + src/sugar3/speech.py | 242 +++++++++++++++++++++++++++++++++++++++++ 2 files changed, 243 insertions(+) create mode 100644 src/sugar3/speech.py diff --git a/src/sugar3/Makefile.am b/src/sugar3/Makefile.am index a67cb468..2784a5ff 100644 --- a/src/sugar3/Makefile.am +++ b/src/sugar3/Makefile.am @@ -16,6 +16,7 @@ sugar_PYTHON = \ network.py \ power.py \ profile.py \ + speech.py \ util.py nodist_sugar_PYTHON = config.py diff --git a/src/sugar3/speech.py b/src/sugar3/speech.py new file mode 100644 index 00000000..203ffe22 --- /dev/null +++ b/src/sugar3/speech.py @@ -0,0 +1,242 @@ +# Copyright (C) 2011 One Laptop Per Child +# +# 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 2 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, write to the Free Software +# Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA + +import os +import logging + +from gi.repository import Gio +from gi.repository import Gst +from gi.repository import Gtk +from gi.repository import Gdk +from gi.repository import GObject + +from sugar3 import power + +DEFAULT_PITCH = 0 + + +DEFAULT_RATE = 0 + + +_SAVE_TIMEOUT = 500 + + +class SpeechManager(GObject.GObject): + + __gtype_name__ = 'SpeechManager' + + __gsignals__ = { + 'play': (GObject.SignalFlags.RUN_FIRST, None, []), + 'pause': (GObject.SignalFlags.RUN_FIRST, None, []), + 'stop': (GObject.SignalFlags.RUN_FIRST, None, []) + } + + MIN_PITCH = -100 + MAX_PITCH = 100 + + MIN_RATE = -100 + MAX_RATE = 100 + + def __init__(self, **kwargs): + GObject.GObject.__init__(self, **kwargs) + self._player = _GstSpeechPlayer() + self._player.connect('play', self._update_state, 'play') + self._player.connect('stop', self._update_state, 'stop') + self._player.connect('pause', self._update_state, 'pause') + self._voice_name = self._player.get_default_voice() + self._pitch = DEFAULT_PITCH + self._rate = DEFAULT_RATE + self._is_playing = False + self._is_paused = False + self._save_timeout_id = -1 + self.restore() + + def _update_state(self, player, signal): + self._is_playing = (signal == 'play') + self._is_paused = (signal == 'pause') + self.emit(signal) + + def get_is_playing(self): + return self._is_playing + + is_playing = GObject.property(type=bool, getter=get_is_playing, + setter=None, default=False) + + def get_is_paused(self): + return self._is_paused + + is_paused = GObject.property(type=bool, getter=get_is_paused, + setter=None, default=False) + + def get_pitch(self): + return self._pitch + + def get_rate(self): + return self._rate + + def set_pitch(self, pitch): + self._pitch = pitch + if self._save_timeout_id != -1: + GObject.source_remove(self._save_timeout_id) + self._save_timeout_id = GObject.timeout_add(_SAVE_TIMEOUT, self.save) + + def set_rate(self, rate): + self._rate = rate + if self._save_timeout_id != -1: + GObject.source_remove(self._save_timeout_id) + self._save_timeout_id = GObject.timeout_add(_SAVE_TIMEOUT, self.save) + + def say_text(self, text): + if text: + self._player.speak(self._pitch, self._rate, self._voice_name, text) + + def say_selected_text(self): + clipboard = Gtk.Clipboard.get(Gdk.SELECTION_PRIMARY) + clipboard.request_text(self.__primary_selection_cb, None) + + def pause(self): + self._player.pause_sound_device() + + def restart(self): + self._player.restart_sound_device() + + def stop(self): + self._player.stop_sound_device() + + def __primary_selection_cb(self, clipboard, text, user_data): + self.say_text(text) + + def save(self): + self._save_timeout_id = -1 + # DEPRECATED + from gi.repository import GConf + client = GConf.Client.get_default() + client.set_int('/desktop/sugar/speech/pitch', self._pitch) + client.set_int('/desktop/sugar/speech/rate', self._rate) + + settings = Gio.Settings('org.sugarlabs.speech') + settings.set_int('pitch', self._pitch) + settings.set_int('rate', self._rate) + logging.debug('saving speech configuration pitch %s rate %s', + self._pitch, self._rate) + return False + + def restore(self): + settings = Gio.Settings('org.sugarlabs.speech') + self._pitch = settings.get_int('pitch') + self._rate = settings.get_int('rate') + logging.debug('loading speech configuration pitch %s rate %s', + self._pitch, self._rate) + + +class _GstSpeechPlayer(GObject.GObject): + + __gsignals__ = { + 'play': (GObject.SignalFlags.RUN_FIRST, None, []), + 'pause': (GObject.SignalFlags.RUN_FIRST, None, []), + 'stop': (GObject.SignalFlags.RUN_FIRST, None, []) + } + + def __init__(self): + GObject.GObject.__init__(self) + self._pipeline = None + + def restart_sound_device(self): + if self._pipeline is None: + logging.debug('Trying to restart not initialized sound device') + return + + power.get_power_manager().inhibit_suspend() + self._pipeline.set_state(Gst.State.PLAYING) + self.emit('play') + + def pause_sound_device(self): + if self._pipeline is None: + return + + self._pipeline.set_state(Gst.State.PAUSED) + power.get_power_manager().restore_suspend() + self.emit('pause') + + def stop_sound_device(self): + if self._pipeline is None: + return + + self._pipeline.set_state(Gst.State.NULL) + power.get_power_manager().restore_suspend() + self.emit('stop') + + def make_pipeline(self, command): + if self._pipeline is not None: + self.stop_sound_device() + del self._pipeline + + self._pipeline = Gst.parse_launch(command) + + bus = self._pipeline.get_bus() + bus.add_signal_watch() + bus.connect('message', self.__pipe_message_cb) + + def __pipe_message_cb(self, bus, message): + if message.type in (Gst.MessageType.EOS, Gst.MessageType.ERROR): + self._pipeline.set_state(Gst.State.NULL) + self._pipeline = None + power.get_power_manager().restore_suspend() + self.emit('stop') + + def speak(self, pitch, rate, voice_name, text): + # TODO workaround for http://bugs.sugarlabs.org/ticket/1801 + if not [i for i in text if i.isalnum()]: + return + + self.make_pipeline('espeak name=espeak ! autoaudiosink') + src = self._pipeline.get_by_name('espeak') + + src.props.text = text + src.props.pitch = pitch + src.props.rate = rate + src.props.voice = voice_name + src.props.track = 2 # track for marks + + self.restart_sound_device() + + def get_all_voices(self): + all_voices = {} + for voice in Gst.ElementFactory.make('espeak', None).props.voices: + name, language, dialect = voice + if dialect != 'none': + all_voices[language + '_' + dialect] = name + else: + all_voices[language] = name + return all_voices + + def get_default_voice(self): + """Try to figure out the default voice, from the current locale ($LANG) + Fall back to espeak's voice called Default.""" + voices = self.get_all_voices() + + locale = os.environ.get('LANG', '') + language_location = locale.split('.', 1)[0].lower() + language = language_location.split('_')[0] + # if the language is es but not es_es default to es_la (latin voice) + if language == 'es' and language_location != 'es_es': + language_location = 'es_la' + + best = voices.get(language_location) or voices.get(language) \ + or 'default' + logging.debug('Best voice for LANG %s seems to be %s', + locale, best) + return best