From 663b6c907c1f75502b1ccc0135a9532634830a88 Mon Sep 17 00:00:00 2001 From: flavio Date: Sat, 04 Aug 2012 16:27:48 +0000 Subject: GnomeSpeak Base --- diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..bbc801c --- /dev/null +++ b/.gitignore @@ -0,0 +1,5 @@ +*.pyc +*.pyo +*.mo +*~ +*.bak diff --git a/JAMediaReproductor.py b/JAMediaReproductor.py new file mode 100644 index 0000000..83d75f6 --- /dev/null +++ b/JAMediaReproductor.py @@ -0,0 +1,361 @@ +#!/usr/bin/env python +# -*- coding: utf-8 -*- + +# JAMediaReproductor.py por: +# Flavio Danesse +# CeibalJAM! - Uruguay +# +# 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 + +# Se remplaza: +# Depends: python-gst0.10, +# gstreamer0.10-plugins-good, +# gstreamer0.10-plugins-ugly, +# gstreamer0.10-plugins-bad, +# gstreamer0.10-ffmpeg + +# Con: +# Depends: python-gi, +# gir1.2-gstreamer-1.0, +# gir1.2-gst-plugins-base-1.0, +# gstreamer1.0-plugins-good, +# gstreamer1.0-plugins-ugly, +# gstreamer1.0-plugins-bad, +# gstreamer1.0-libav + +# https://wiki.ubuntu.com/Novacut/GStreamer1.0#Using_GStreamer_1.0_from_Python +# http://gstreamer.freedesktop.org/data/doc/gstreamer/head/gstreamer/html/GstBus.html#gst-bus-create-watch +# http://www.roojs.org/seed/gir-1.1-gtk-2.0/Gst.MessageType.html#expand + +import os + +import gi +gi.require_version('Gst', '1.0') + +from gi.repository import GObject +from gi.repository import Gst +from gi.repository import GstVideo + +GObject.threads_init() +Gst.init(None) + +class JAMediaReproductor(GObject.GObject): + """ + Reproductor de Audio, Video y Streaming de + Radio y Television. Implementado sobre: + + python 2.7.3 + Gtk 3 + Gstreamer 1.0 + """ + + __gsignals__ = {"endfile":(GObject.SIGNAL_RUN_FIRST, + GObject.TYPE_NONE, []), + "estado":(GObject.SIGNAL_RUN_FIRST, GObject.TYPE_NONE, + (GObject.TYPE_STRING,)), + "newposicion":(GObject.SIGNAL_RUN_FIRST, GObject.TYPE_NONE, + (GObject.TYPE_INT,)), + "volumen":(GObject.SIGNAL_RUN_FIRST, GObject.TYPE_NONE, + (GObject.TYPE_FLOAT,))} + + # Estados: playing, paused, None + + def __init__(self, ventana_id): + """ Recibe el id de un DrawingArea + para mostrar el video. """ + + GObject.GObject.__init__(self) + self.name = "JAMediaReproductor" + self.ventana_id = ventana_id + self.pipeline = None + self.estado = None + + self.duracion = 0 + self.posicion = 0 + self.actualizador = None + + self.player = None + self.bus = None + + self.set_pipeline() + + def set_pipeline(self): + """Crea el pipe de Gst. (playbin)""" + + if self.pipeline: del(self.pipeline) + + self.pipeline = Gst.Pipeline() + self.bus = self.pipeline.get_bus() + self.bus.add_signal_watch() + self.bus.connect('message', self.on_mensaje) + + self.bus.enable_sync_message_emission() + self.bus.connect('sync-message', self.sync_message) + + self.player = Gst.ElementFactory.make("playbin", "player") + self.pipeline.add(self.player) + + def sync_message(self, bus, mensaje): + """Captura los mensajes en el bus del pipe Gst.""" + + try: + if mensaje.get_structure().get_name() == 'prepare-window-handle': + mensaje.src.set_window_handle(self.ventana_id) + return + except: + pass + + if mensaje.type == Gst.MessageType.STATE_CHANGED: + old, new, pending = mensaje.parse_state_changed() + if old == Gst.State.PAUSED and new == Gst.State.PLAYING: + if self.estado != new: + self.estado = new + self.emit("estado", "playing") + self.new_handle(True) + return + elif old == Gst.State.READY and new == Gst.State.PAUSED: + if self.estado != new: + self.estado = new + self.emit("estado", "paused") + self.new_handle(False) + return + elif old == Gst.State.READY and new == Gst.State.NULL: + if self.estado != new: + self.estado = new + self.emit("estado", "None") + self.new_handle(False) + return + elif old == Gst.State.PLAYING and new == Gst.State.PAUSED: + if self.estado != new: + self.estado = new + self.emit("estado", "paused") + self.new_handle(False) + return + elif old == Gst.State.NULL and new == Gst.State.READY: + pass + elif old == Gst.State.PAUSED and new == Gst.State.READY: + pass + else: + #print ">>>", old, new, pending + return + + elif mensaje.type == Gst.MessageType.ASYNC_DONE: + #print mensaje.get_structure().get_name() + return + elif mensaje.type == Gst.MessageType.NEW_CLOCK: + return + elif mensaje.type == Gst.MessageType.STREAM_STATUS: + #print mensaje.parse_stream_status() + return + elif mensaje.type == Gst.MessageType.TAG: + return + elif mensaje.type == Gst.MessageType.ERROR: + err, debug = mensaje.parse_error() + print "***", 'sync_message' + print err, debug + self.new_handle(False) + #self.pipeline.set_state(Gst.State.NULL) + #self.pipeline.set_state(Gst.State.READY) + #return self.set_pipeline() + return + elif mensaje.type == Gst.MessageType.EOS: + return + else: + try: + nombre = mensaje.get_structure().get_name() + if nombre == "playbin-stream-changed": + #print "Nuevo src:", mensaje.get_structure().to_string() + pass + elif nombre == "have-window-handle": + pass + elif nombre == "prepare-window-handle": + pass + else: + pass + except: + print "sync_message", mensaje.type + return + + def on_mensaje(self, bus, mensaje): + """Captura los mensajes en el bus del pipe Gst.""" + + #if mensaje.type == Gst.MessageType.ASYNC_DONE: + # print mensaje.get_structure().get_name() + #elif mensaje.type == Gst.MessageType.NEW_CLOCK: + # pass + + if mensaje.type == Gst.MessageType.ELEMENT: + nombre = mensaje.get_structure().get_name() + # playbin-stream-changed , prepare-window-handle, have-window-handle + #if nombre == 'prepare-window-handle': + # mensaje.src.set_window_handle(self.ventana_id) + #else: + # print nombre + + ''' + elif mensaje.type == Gst.MessageType.STATE_CHANGED: + old, new, pending = mensaje.parse_state_changed() + if old == Gst.State.PAUSED and new == Gst.State.PLAYING: + if self.estado != new: + self.estado = new + #self.emit("estado", "playing") + elif old == Gst.State.READY and new == Gst.State.PAUSED: + if self.estado != new: + self.estado = new + #self.emit("estado", "paused") + else: + if self.F: print old, new, pending + pass + elif mensaje.type == Gst.MessageType.STREAM_STATUS: + pass + elif mensaje.type == Gst.MessageType.TAG: + #print mensaje.get_structure().to_string() + pass''' + + if mensaje.type == Gst.MessageType.EOS: + #self.pipeline.seek_simple(Gst.Format.TIME, + #Gst.SeekFlags.FLUSH | Gst.SeekFlags.KEY_UNIT, 0) + self.new_handle(False) + self.set_pipeline() + self.emit("endfile") + + elif mensaje.type == Gst.MessageType.QOS: + pass + elif mensaje.type == Gst.MessageType.WARNING: + print mensaje.get_structure().to_string() + #self.new_handle(False) + #self.pipeline.set_state(Gst.State.NULL) + #self.set_pipeline() + + elif mensaje.type == Gst.MessageType.ERROR: + err, debug = mensaje.parse_error() + print "***", 'sync_message' + print err, debug + self.new_handle(False) + #self.pipeline.set_state(Gst.State.NULL) + #self.set_pipeline() + self.pipeline.set_state(Gst.State.READY) + + elif mensaje.type == Gst.MessageType.LATENCY: + #print mensaje.type + pass + else: + #print "on_mensaje", mensaje.type + pass + + def load(self, uri): + """Carga un archivo o stream en el pipe de Gst.""" + + self.stop() + if os.path.exists(uri): + direccion = Gst.filename_to_uri(uri) + self.player.set_property("uri", direccion) + self.play() + else: + # FIXME: Funciona con la radio pero no con la Tv + direccion = uri + #Gst.uri_get_protocol(uri) + if Gst.uri_is_valid(uri): + self.player.set_property("uri", direccion) + self.play() + #print Gst.uri_protocol_is_supported(uri, Gst.uri_get_protocol(uri)) + #print Gst.uri_protocol_is_valid(Gst.uri_get_protocol(uri)) + #self.player.set_property("buffer-size", 1024) + #self.player.set_property("force-aspect-ratio", False) + + def play(self): + """Pone el pipe de Gst en Gst.State.PLAYING""" + + self.pipeline.set_state(Gst.State.PLAYING) + + def stop(self): + """Pone el pipe de Gst en Gst.State.NULL""" + + self.pipeline.set_state(Gst.State.NULL) + self.pipeline.set_state(Gst.State.READY) + + def pause(self): + """Pone el pipe de Gst en Gst.State.PAUSED""" + + self.pipeline.set_state(Gst.State.PAUSED) + + def pause_play(self): + """Llama a play() o pause() + segun el estado actual del pipe de Gst.""" + + if self.estado == Gst.State.PAUSED \ + or self.estado == Gst.State.NULL \ + or self.estado == Gst.State.READY: + self.play() + elif self.estado == Gst.State.PLAYING: + self.pause() + else: + print self.estado + + def new_handle(self, reset): + """Elimina o reinicia la funcion que + envia los datos de actualizacion para + la barra de progreso del reproductor.""" + + if self.actualizador: + GObject.source_remove(self.actualizador) + self.actualizador = None + if reset: + self.actualizador = GObject.timeout_add(300, self.handle) + + def handle(self): + """Funcion que envia los datos de actualizacion para + la barra de progreso del reproductor.""" + + bool1, valor1 = self.pipeline.query_duration(Gst.Format.TIME) + bool2, valor2 = self.pipeline.query_position(Gst.Format.TIME) + + duracion = float(valor1) + posicion = float(valor2) + + pos = 0 + try: + pos = int(posicion * 100 / duracion) + except: + pass + + if pos < 0 or pos > self.duracion: return True + + if self.duracion != duracion: self.duracion = duracion + + if pos != self.posicion: + self.posicion = pos + self.emit("newposicion", self.posicion) + # print "***", gst.video_convert_frame(self.player.get_property("frame")) + + return True + + def set_position(self, posicion): + """Funcion que permite desplazarse por + el archivo que se esta reproduciendo.""" + + if self.duracion < posicion: + self.emit("newposicion", self.posicion) + return + posicion = self.duracion * posicion / 100 + self.pipeline.seek_simple(Gst.Format.TIME, + Gst.SeekFlags.FLUSH, posicion) + + def get_volumen(self): + pass + + def set_volumen(self, valor): + pass + \ No newline at end of file diff --git a/MplayerReproductor.py b/MplayerReproductor.py new file mode 100644 index 0000000..366fb1d --- /dev/null +++ b/MplayerReproductor.py @@ -0,0 +1,346 @@ +#!/usr/bin/env python +# -*- coding: utf-8 -*- + +# MplayerReproductor.py por: +# Flavio Danesse +# CeibalJAM! - Uruguay + +# 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 time +import os +import subprocess +#import platform + +import gi +from gi.repository import GObject + +STDOUT = "/tmp/jamediaout%d" % time.time() +STDERR = "/dev/null" +MPLAYER = "mplayer" + +#if "olpc" in platform.platform(): MPLAYER = "./mplayer" +#if "Strawberry" in platform.platform(): MPLAYER = "./mplayer" + +class MplayerReproductor(GObject.GObject): + """ + Reproductor de Audio, Video y Streaming de + Radio y Television. Implementado sobre: + + python 2.7.3 + Gtk 3 + mplayer (a traves de python.subprocess) + """ + + __gsignals__ = {"endfile":(GObject.SIGNAL_RUN_FIRST, + GObject.TYPE_NONE, []), + "estado":(GObject.SIGNAL_RUN_FIRST, GObject.TYPE_NONE, + (GObject.TYPE_STRING,)), + "newposicion":(GObject.SIGNAL_RUN_FIRST, GObject.TYPE_NONE, + (GObject.TYPE_INT,)), + "volumen":(GObject.SIGNAL_RUN_FIRST, GObject.TYPE_NONE, + (GObject.TYPE_FLOAT,))} + + # Estados: playing, paused, None + + def __init__(self, ventana_id): + """ Recibe el id de un DrawingArea + para mostrar el video. """ + + GObject.GObject.__init__(self) + self.name = "MplayerReproductor" + self.ventana_id = ventana_id + self.mplayer = None + self.salida = None + self.entrada = None + self.estado = None + self.duracion = 0 + self.posicion = 0 + self.volumen = 0 + self.actualizador = None + self.uri = None + + def stop(self): + """Detiene todo.""" + + try: + if self.entrada: + self.entrada.write('%s 0\n' % "quit") + self.entrada.flush() + self.new_handle(False) + except Exception, e: + #print "HA OCURRIDO UN ERROR EN QUIT DEL REPRODUCTOR", e + pass + self.posicion = 0 + if os.path.exists(STDOUT): os.unlink(STDOUT) + import commands + commands.getoutput('killall mplayer') + self.estado = None + self.emit("estado", "None") + + def load(self, uri): + """Carga y Reproduce un archivo o streaming.""" + + self.stop() + self.uri = uri + if os.path.exists(self.uri): + uri = "%s%s%s" % ("\"", self.uri, "\"") + + cache_pantalla = "%s -cache %i -wid %i" % (MPLAYER, 1024, self.ventana_id) + estructura = "%s -slave -idle -nolirc -rtc -nomouseinput -noconsolecontrols -nojoystick" % (cache_pantalla) + self.mplayer = subprocess.Popen(estructura, shell = True, stdin = subprocess.PIPE, + stdout = open(STDOUT,"w+b"), stderr=open(STDOUT,"r+b"), universal_newlines=True) + self.entrada = self.mplayer.stdin + self.salida = open(STDOUT,"r") + self.entrada.write("loadfile %s 0\n" % uri) + self.entrada.flush() + self.new_handle(True) + + def handle(self): + """Consulta el estado y progreso del + la reproduccion actual.""" + + if not self.entrada.closed: + # Control por tiempo + #self.entrada.write("%s 0\n" % ("get_time_length")) + #self.entrada.flush() + #duracion = self.salida.readline() + #if "ANS_LENGTH" in duracion: + # duracion = float(duracion.split("=")[1]) # Duración en Segundos + # print "dur", duracion + + #self.entrada.write("%s 0\n" % ("get_time_pos")) + #self.entrada.flush() + #posicion = self.salida.readline() + #if "ANS_TIME_POSITION" in posicion: + # posicion = float(posicion.split("=")[1]) # Posición en Segundos + # print "pos", posicion + + self.entrada.write("%s 0\n" % ("get_property percent_pos")) + self.entrada.flush() + linea = self.salida.readline() + if linea: + if "ANS_percent_pos" in linea: + "Información sobre el porcentaje Reproducido hasta el momento. Ejemplo:" + "ANS_percent_pos=0" + self.get_progress_in_mplayer(linea) + self.get_volumen() + + elif "Video: no video" in linea or "Audio only file format detected" in linea: + "Cuando no hay video en la fuente. Ejemplo" + "Audio only file format detected." + "Video: no video" + #self.emit("video", False) + pass + + elif "Cache" in linea: + "Información Sobre Carga de caché. Ejemplo:" + "Cache fill: 6.25% (65536 bytes)" + #self.get_progress_cache_in_mplayer(linea) + pass + + elif "Movie-Aspect" in linea: + "Información sobre el aspecto del video. Ejemplo:" + "Movie-Aspect is 1.78:1 - prescaling to correct movie aspect." + #self.emit("video", True) + pass + + elif "Starting playback" in linea: + "Cuando comienza la Reproducción. Ejemplo:" + "Starting playback..." + self.estado = "playing" + self.emit("estado", "playing") + + elif "AO:" in linea: + "Información Sobre Audio en la pista. Ejemplo:" + "AO: [pulse] 44100Hz 2ch s16le (2 bytes per sample)" + pass + + elif "VO:" in linea: + "Información sobre Video en la pista. Ejemplo:" + "VO: [xv] 428x240 => 428x240 Planar YV12" + pass + + elif "Resolving" in linea: + "Información sobre Resolución de Streamings. Ejemplo:" + "Resolving radio1.oceanofm.com for AF_INET6..." + pass + + elif "Connecting" in linea: + "Información sobre Conexión a un Streaming. Ejemplo:" + "Connecting to server main-office.rautemusik.fm[87.230.101.9]: 80..." + pass + + elif "Name" in linea: + "El nombre de una streaming de Radio. Ejemplo:" + "Name : #MUSIK.MAIN - WWW.RAUTEMUSIK.FM - 24H TOP 40 POP HITS 80S 90S DANCE HOUSE ROCK RNB AND MORE!" + pass + + elif "Playing" in linea: + "La Pista que se está reproduciendo. Ejemplo:" + "Playing /media/4E432D364BC64012/E - Videos/Tylor swift/Back to December-Taylor Swift Lyrics." + pass + + elif "Genre" in linea or "Website" in linea or "Bitrate" in linea: + "Información Sobre un Streaming de Radio. Ejemplo:" + "Genre : Pop Rock Top 40 RnB 80s" + "Website: http://www.RauteMusik.FM/" + "Bitrate: 128kbit/s" + pass + + elif "Opening" in linea or "AUDIO" in linea or "Selected" in linea: + "Información sobre Codecs Utilizados. Ejemplo:" + "Opening video decoder: [ffmpeg] FFmpeg's libavcodec codec family" + "Selected video codec: [ffh264] vfm: ffmpeg (FFmpeg H.264)" + "Opening audio decoder: [ffmpeg] FFmpeg/libavcodec audio decoders" + "AUDIO: 44100 Hz, 2 ch, s16le, 98.6 kbit/6.99% (ratio: 12323->176400)" + "Selected audio codec: [ffaac] afm: ffmpeg (FFmpeg AAC (MPEG-2/MPEG-4 Audio))" + pass + + else: + "Información Diversa. Ejemplo:" + "Failed to open /dev/rtc: Permission denied (it should be readable by the us" + "Unsupported PixelFormat 61" + "Unsupported PixelFormat 53" + "Unsupported PixelFormat 81" + "eo (h264), -vid 0" + "[lavf] stream 1: audio (aac), -aid 0" + "VIDEO: [H264] 640x480 0bpp 30.000 fps 218.3 kbps (26.6 kbyte/s)" + "Clip info:" + "starttime: 0" + "totalduration: 226" + "totaldatarate: 338" + "bytelength: 9570679" + "canseekontime: true" + "sourcedata: BC8280065HH1341966475963833" + "purl: " + "pmsg: " + "Load subtitles in /media/4E432D364BC64012/E - Videos/Tylor swift/" + "==========================================================================" + "libavcodec version 53.35.0 (external)" + "Mismatching header version 53.32.2" + "==========================================================================" + "==========================================================================" + "==========================================================================" + "A: 0.0 V: 0.0 A-V: 0.014 ct: 0.000 0/ 0 ??% ??% ??,?% 0 0 90%" + "A: 123.2 V: 123.2 A-V: -0.000 ct: 0.033 0/ 0 2% 1% 0.4% 0 0 50%" + + "Cuando no se puede Acceder a un Streaming. Ejemplo:" + "Failed to get value of property 'percent_pos'." + "Failed to get value of property 'percent_ANS_ERFailed to get value of" + "prANS_ERROR=PROPERTY_UNAFailed toANS_ERROR=PROPERTY_UNAVAILABLE" + "ANS_ERROR=PROPERTY_UNAVAILABLE" + pass + + return True + + def get_progress_in_mplayer(self, linea): + """Obtiene el progreso de la reproduccion y lo + envia en una señal para actualizar la barra de + progreso.""" + + pos = 0 + try: + if "Cache size" in linea: return + pos = int(linea.split('=')[1]) + if pos != self.posicion: + self.posicion = pos + self.emit("newposicion", self.posicion) + if self.posicion >= 100: + self.emit("endfile") + except Exception, e: + print "Error en Progreso de Reproducción: %s" % (e) + + def pause_play(self): + """Llama a play() o pause() + segun el estado actual del reproductor.""" + + try: + if self.entrada: + if self.estado == "playing": # pausa + self.pause() + elif self.estado == "paused": + self.pause(True) + self.estado = "playing" + self.emit("estado", "playing") + else: + #if self.uri: self.load(self.uri) + pass + except Exception, e: + print "HA OCURRIDO UN ERROR EN PAUSE_PLAY DEL REPRODUCTOR", e + + def pause(self, reset = False): + """Pone en pause o unpause a mplayer""" + + self.entrada.write('pause 0\n') + self.entrada.flush() + self.new_handle(reset) + self.estado = "paused" + self.emit("estado", "paused") + + def play(self): + """No hace nada. mplayer utiliza: + pause, unpause y load en lugar de play.""" + + pass + + def new_handle(self, reset): + """Elimina o reinicia la funcion que + envia los datos de actualizacion para + la barra de progreso del reproductor.""" + + if self.actualizador: + GObject.source_remove(self.actualizador) + self.actualizador = None + if reset: + self.actualizador = GObject.timeout_add(35, self.handle) + + def set_position(self, posicion): + """Funcion que permite desplazarse por + el archivo que se esta reproduciendo.""" + + # FIXME: Actualmente no funciona bien + posicion = int(posicion) + if posicion != self.posicion: + self.posicion = posicion + self.entrada.write('seek %s %i 0\n' % (posicion, 1)) + self.entrada.flush() + + def get_volumen(self): + """Obtiene el volumen de reproducción. + Lo hace solo al reproducir el primer archivo + o streaming y envía el dato para actualizar + el control de volúmen.""" + + if self.volumen != 0: return + if self.entrada: + self.entrada.write("%s 0\n" % ("get_property volume")) + self.entrada.flush() + linea = self.salida.readline() + if "ANS_volume" in linea: + valor = float(linea.split("=")[1]) + if self.volumen == 0: + self.volumen = valor + self.emit('volumen', valor) + + def set_volumen(self, valor): + """Cambia el volúmen de Reproducción.""" + + if self.entrada: + if valor != self.volumen: + self.volumen = valor + self.entrada.write("%s %s0\n" % ("set_property volume", valor)) + self.entrada.flush() + \ No newline at end of file diff --git a/Sound.py b/Sound.py new file mode 100644 index 0000000..546c5b2 --- /dev/null +++ b/Sound.py @@ -0,0 +1,81 @@ +#!/usr/bin/env python +# -*- 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 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 gi +from gi.repository import GObject + +from JAMediaReproductor import JAMediaReproductor +from MplayerReproductor import MplayerReproductor + +PITCH_MAX = 200 +RATE_MAX = 200 +wavpath = "/tmp/speak.wav" + +def make_file(pitch, rate, voice, text): + """Genera el archivo de audio.""" + + rate = 60 + int(((175 - 80) * 2) * rate / RATE_MAX) + command = "espeak -w %s -p %s -s %s -v%s \"%s\"" % (wavpath, + str(pitch), str(rate), voice, text) + ret = os.system(command) + file = open(wavpath, "r") + file.flush() + #print file.readlines() + os.fsync(file) + file.close() + return ret + +class Sound(GObject.GObject): + """Interactua entre espeak y JAMediaReproductor.""" + + def __init__(self, xid): + + GObject.GObject.__init__(self) + + self.pitch = 0 + self.rate = 0 + self.voice = "es" + self.text = "" + self.estado = None + + self.player = MplayerReproductor(xid) #JAMediaReproductor(xid) + + def set_voice(self, value): + """Cuando cambia el lenguaje.""" + + self.voice = value + + def set_pitch(self, value): + """Cuando cambia pitch.""" + + self.pitch = value + + def set_rate(self, value): + """Cuando cambia rate.""" + + self.rate = value + + def speak(self, text): + """Cuando habla.""" + + self.text = text + + if not make_file(self.pitch, self.rate, self.voice, self.text): + self.player.load(wavpath) + \ No newline at end of file diff --git a/Speak.py b/Speak.py new file mode 100644 index 0000000..9a0a5a2 --- /dev/null +++ b/Speak.py @@ -0,0 +1,197 @@ +#!/usr/bin/env python +# -*- 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 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 sys +import os + +import gi +from gi.repository import Gtk +from gi.repository import Gdk +from gi.repository import GObject +from gi.repository import GdkX11 + +from Widgets import Speaking +from Widgets import Toolbar +from Widgets import ToolbarPitchRate +from Widgets import ToolbarFace +from Sound import Sound + +BASE = os.path.dirname(__file__) + +GObject.threads_init() +Gdk.threads_init() + +class Speak(Gtk.Window): + + def __init__(self): + Gtk.Window.__init__(self) + + self.set_title("Hablar con Sara") + self.set_icon_from_file(os.path.join(BASE, "icons", "face.svg")) + self.set_resizable(True) + self.set_border_width(0) + self.set_size_request(640, 480) + self.modify_bg(0, Gdk.Color(65000, 28260, 30516)) + + self.add_events( + Gdk.EventMask.POINTER_MOTION_HINT_MASK | + Gdk.EventMask.POINTER_MOTION_MASK) + + self.toolbar = None + self.toolbarpitchrate = None + self.toolbarface = None + self.notebook = None + self.speaking = None + self.sound = None + + self.dinamic_toolbars = [] + + self.setup_init() + + def setup_init(self): + """Se crean los objetos y se empaqueta todo.""" + + self.toolbar = Toolbar() + self.toolbarpitchrate = ToolbarPitchRate() + self.toolbarface = ToolbarFace() + self.notebook = Gtk.Notebook() + self.notebook.set_show_border(False) + self.notebook.set_show_tabs(False) + + vbox = Gtk.Box(orientation = Gtk.Orientation.VERTICAL) + vbox.pack_start(self.toolbar, False, False,0) + vbox.pack_start(self.toolbarpitchrate, False, False,0) + vbox.pack_start(self.toolbarface, False, False,0) + + self.speaking = Speaking() # Pagina inicial en el notebook + self.notebook.append_page(self.speaking, Gtk.Label("")) + vbox.pack_start(self.notebook, True, True,0) + + self.dinamic_toolbars = [self.toolbarpitchrate, self.toolbarface] + + self.add(vbox) + + self.show_all() + + map(self.hide_widgets, self.dinamic_toolbars) + + xid = self.speaking.face.get_property('window').get_xid() + self.sound = Sound(xid) + + self.sound.set_pitch(100) + self.sound.set_rate(100) + self.sound.set_voice('es') + + self.connect("motion_notify_event", self.mouse_moved) + + self.toolbar.connect("switch-voice", self.switch_voice) + #self.toolbar.connect("switch-desktop", # cambiar entre speaking, chat y robot) + self.toolbar.connect("switch-toolbar", self.switch_toolbar) + + self.toolbarpitchrate.connect("pitch", self.switch_pitch) + self.toolbarpitchrate.connect("rate", self.switch_rate) + + self.toolbarface.connect("number-eyes", self.number_eyes) + self.toolbarface.connect("switch-type-mouth", self.switch_type_mouth) + self.toolbarface.connect("switch-type-eyes", self.switch_type_eyes) + + self.speaking.connect("speak", self.speak) + + self.connect("delete_event", self.delete_event) + self.connect("destroy", self.salir) + + def speak(self, widget, value): + """Cuando el usuario ingresa texto para hablar.""" + + self.sound.speak(value) + + def switch_pitch(self, widget, value): + """cuando cambia pitch.""" + + self.sound.set_pitch(value) + self.sound.speak("Campo Modificado.") + + def switch_rate(self, widget, value): + """Cuando cambia rate.""" + + self.sound.set_rate(value) + self.sound.speak("Velocidad Modificada.") + + def number_eyes(self, widget, value): + """Recibe la cantidad de ojos que deben dibujarse.""" + + self.speaking.face.set_number_eyes(value) + self.sound.speak("Ojos Modificados.") + + def switch_type_mouth(self, widget, value): + """Recibe el tipo de dibujo para la boca.""" + + self.speaking.face.set_type_mouth(value) + self.sound.speak("Boca Modificada.") + + def switch_type_eyes(self, widget, value): + """Recibe el tipo de ojos a dibujar.""" + + self.speaking.face.set_type_eyes(value) + self.sound.speak("Ojos Modificados.") + + def switch_toolbar(self, widget, value): + """Para que se muestre la toolbar pitch-rate o face""" + + if value == "pitch-rate": + if self.toolbarpitchrate.get_visible(): + self.toolbarpitchrate.hide() + else: + map(self.hide_widgets, self.dinamic_toolbars) + self.toolbarpitchrate.show() + + elif value == "face": + if self.toolbarface.get_visible(): + self.toolbarface.hide() + else: + map(self.hide_widgets, self.dinamic_toolbars) + self.toolbarface.show() + + def hide_widgets(self, objeto): + """Esta funcion es llamada desde self.switch_toolbar()""" + + if objeto.get_visible(): objeto.hide() + + def switch_voice(self, widget, value): + """Cuando cambia el lenguaje.""" + + self.sound.set_voice(value) + self.sound.speak("Voz Modificada.") + + def delete_event(self, widget = None, event = None, data = None): + self.salir() + return False + + def salir(self, widget = None, senial = None): + sys.exit(0) + + def mouse_moved(self, widget, event): + """Le dice a la cara hacia donde mirar.""" + + pos = (event.x, event.y) + self.speaking.look_at(pos) + +if __name__=="__main__": + Speak() + Gtk.main() + \ No newline at end of file diff --git a/Widgets.py b/Widgets.py new file mode 100644 index 0000000..fb87cbf --- /dev/null +++ b/Widgets.py @@ -0,0 +1,423 @@ +#!/usr/bin/env python +# -*- 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 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 commands + +import gi +from gi.repository import Gtk +from gi.repository import Gdk +from gi.repository import Pango +from gi.repository import GdkPixbuf +from gi.repository import GObject + +from face import Face + +BASE = os.path.dirname(__file__) + +PITCH_MAX = 200 +RATE_MAX = 200 + +def get_voices(): + """Devuelve un diccionario del tipo: {"portugal": "pt-pt", . . .}""" + + ret = commands.getoutput('espeak --voices') + + voces = [] + for linea in ret .split("\n"): + voz = linea.split(" ") + + vv = [] + for valor in voz: + if valor: vv.append(valor) + + voces.append(vv) + + voces = voces[1:] + + voices = {} + for voz in voces: + if voz[2] != "M": + voices[voz[2]] = voz[1] + else: + voices[voz[3]] = voz[1] + + return voices + +def get_separador(draw = False, ancho = 0, expand = False): + """ Devuelve un separador generico.""" + + separador = Gtk.SeparatorToolItem() + separador.props.draw = draw + separador.set_size_request(ancho, -1) + separador.set_expand(expand) + return separador + +def get_boton(archivo, flip = False, color = Gdk.Color(65000, 65000, 65000)): + """ Devuelve un toolbarbutton generico.""" + + boton = Gtk.ToolButton() + imagen = Gtk.Image() + pixbuf = GdkPixbuf.Pixbuf.new_from_file_at_size(archivo, 32, 32) + if flip: pixbuf = pixbuf.flip(True) + imagen.set_from_pixbuf(pixbuf) + imagen.modify_bg(0, color) + boton.set_icon_widget(imagen) + imagen.show() + boton.show() + return boton + +class Speaking(Gtk.Box): + """Cara con Entrada de texto.""" + + __gsignals__ = {"speak":(GObject.SIGNAL_RUN_FIRST, + GObject.TYPE_NONE, (GObject.TYPE_STRING, ))} + + def __init__(self): + + Gtk.Box.__init__(self, orientation=Gtk.Orientation.VERTICAL) + + self.entrycombo = None + self.entry = None + self.face = None + + self.entrycombo = Gtk.ComboBoxText.new_with_entry() + + self.entry = self.entrycombo.get_child() + self.entry.can_activate_accel(True) + + font = Pango.FontDescription('sans bold 24') + self.entry.modify_font(font) + + self.face = Face() + + self.pack_start(self.face, True, True, 0) + self.pack_start(self.entrycombo, False, True, 0) + + self.show_all() + + self.entrycombo.connect("changed", self._combo_changed_cb) + + self.entry.connect('activate', self._entry_activate_cb) + self.entry.connect("key-press-event", self._entry_key_press_cb) + self.entry.connect("move-cursor", self._cursor_moved_cb) + self.entry.connect("changed", self._cursor_moved_cb) + + def _cursor_moved_cb(self, entry): + """Mira lo que el usuario escribe.""" + + index = entry.get_property('cursor_position') + layout = entry.get_layout() + pos = layout.get_cursor_pos(index) + x = pos[0].x / Pango.SCALE - entry.get_property('scroll_offset') + y = entry.get_allocation().y + self.face.look_at(pos=(x, y)) + + def _combo_changed_cb(self, combo): + if not self.entry.is_focus(): + self.entry.grab_focus() + self.entry.select_region(0, -1) + + def look_at(self, pos): + """Le dice a la cara hacia donde mirar.""" + + self.face.look_at(pos) + + def _entry_activate_cb(self, entry): + """Cuando el usuario presiona enter en + la entrada de texto.""" + + text = entry.get_text() + + if text: + self.emit('speak', text) + + history = self.entrycombo.get_model() + if len(history) == 0 or history[-1][0] != text: + self.entrycombo.append_text(text) + while len(history) > 20: + self.entrycombo.remove_text(0) + self.entrycombo.set_active(len(history) - 1) + entry.select_region(0, -1) + + def _entry_key_press_cb(self, combo, event): + """Para navegar el historial de entradas + en la entrada de texto, con el teclado.""" + + keyname = Gdk.keyval_name(event.keyval) + index = self.entrycombo.get_active() + + if keyname == "Up": + if index > 0: index -= 1 + self.entrycombo.set_active(index) + self.entry.select_region(0, -1) + return True + + elif keyname == "Down": + if index < len(self.entrycombo.get_model()) - 1: index += 1 + self.entrycombo.set_active(index) + self.entry.select_region(0, -1) + return True + + return False + + +class Toolbar(Gtk.Toolbar): + """Toolbar Principal.""" + + __gsignals__ = {"switch-voice":(GObject.SIGNAL_RUN_FIRST, + GObject.TYPE_NONE, (GObject.TYPE_STRING, )), + "switch-desktop":(GObject.SIGNAL_RUN_FIRST, + GObject.TYPE_NONE, (GObject.TYPE_STRING, )), + "switch-toolbar":(GObject.SIGNAL_RUN_FIRST, + GObject.TYPE_NONE, (GObject.TYPE_STRING, ))} + + def __init__(self): + + Gtk.Toolbar.__init__(self) + self.modify_bg(0, Gdk.Color(0, 0, 0)) + + self.voices = get_voices() + + self.insert(get_separador(draw = False, ancho = 32, expand = False), -1) + + archivo = os.path.join(BASE, "icons", "face.svg") + boton = get_boton(archivo, flip = False, color = Gdk.Color(0, 0, 0)) + self.insert(boton, -1) + + archivo = os.path.join(BASE, "icons", "mode-type.svg") + boton = get_boton(archivo, flip = False, color = Gdk.Color(0, 0, 0)) + self.insert(boton, -1) + #boton.connect("clicked", self.emit_switch_desktop, "speaking") + + archivo = os.path.join(BASE, "icons", "mode-robot.svg") + boton = get_boton(archivo, flip = False, color = Gdk.Color(0, 0, 0)) + self.insert(boton, -1) + #boton.connect("clicked", self.emit_switch_desktop, "robot") + + archivo = os.path.join(BASE, "icons", "mode-chat.svg") + boton = get_boton(archivo, flip = False, color = Gdk.Color(0, 0, 0)) + self.insert(boton, -1) + #boton.connect("clicked", self.emit_switch_desktop, "chat") + + item = Gtk.ToolItem() + combo = Gtk.ComboBoxText() + voces = self.voices.keys() + for key in sorted(voces): + combo.append_text(key) + combo.set_active(0) + combo.connect('changed', self.emit_switch_voice) + item.add(combo) + self.insert(item, -1) + + archivo = os.path.join(BASE, "icons", "voice.svg") + boton = get_boton(archivo, flip = False, color = Gdk.Color(0, 0, 0)) + self.insert(boton, -1) + boton.connect("clicked", self.emit_switch_toolbar, "pitch-rate") + + archivo = os.path.join(BASE, "icons", "face.svg") + boton = get_boton(archivo, flip = False, color = Gdk.Color(0, 0, 0)) + self.insert(boton, -1) + boton.connect("clicked", self.emit_switch_toolbar, "face") + + self.show_all() + + def emit_switch_desktop(self, widget, value): + """Cambiar entre chat, speaking y robot.""" + + self.emit("switch-desktop", value) + + def emit_switch_toolbar(self, widget, value): + """Para que se muestre la toolbar pitch-rate o face.""" + + self.emit("switch-toolbar", value) + + def emit_switch_voice(self, widget): + """Cuando cambia el lenguaje.""" + + self.emit("switch-voice", self.voices[widget.get_active_text()]) + +class ToolbarPitchRate(Gtk.Toolbar): + """Toolbar con opciones pitch y rate.""" + + __gsignals__ = {"pitch":(GObject.SIGNAL_RUN_FIRST, + GObject.TYPE_NONE, (GObject.TYPE_INT, )), + "rate":(GObject.SIGNAL_RUN_FIRST, + GObject.TYPE_NONE, (GObject.TYPE_INT, ))} + + def __init__(self): + + Gtk.Toolbar.__init__(self) + self.modify_bg(0, Gdk.Color(0, 0, 0)) + + self.pitch = 0 + self.rate = 0 + + self.insert(get_separador(draw = False, ancho = 32, expand = False), -1) + + item = Gtk.ToolItem() + label = Gtk.Label("Pitch:") + label.modify_fg(0, Gdk.Color(65000, 65000, 65000)) + item.add(label) + self.insert(item, -1) + + self.insert(get_separador(draw = False, ancho = 5, expand = False), -1) + + item = Gtk.ToolItem() + pitchadj = Gtk.Adjustment(self.pitch, 0, + PITCH_MAX, 1, PITCH_MAX / 10, 0) + pitchbar = Gtk.Scale(orientation=Gtk.Orientation.HORIZONTAL) + pitchbar.set_adjustment(pitchadj) + pitchbar = Gtk.Scale(orientation=Gtk.Orientation.HORIZONTAL) + pitchbar.set_adjustment(pitchadj) + pitchbar.set_draw_value(False) + pitchbar.set_size_request(240, 15) + item.add(pitchbar) + self.insert(item, -1) + + self.insert(get_separador(draw = False, ancho = 5, expand = False), -1) + + item = Gtk.ToolItem() + label = Gtk.Label("Rate:") + label.modify_fg(0, Gdk.Color(65000, 65000, 65000)) + item.add(label) + self.insert(item, -1) + + self.insert(get_separador(draw = False, ancho = 5, expand = False), -1) + + item = Gtk.ToolItem() + rateadj = Gtk.Adjustment(self.rate, 0, + RATE_MAX, 1, RATE_MAX / 10, 0) + ratebar = Gtk.Scale(orientation=Gtk.Orientation.HORIZONTAL) + ratebar.set_adjustment(rateadj) + ratebar.set_draw_value(False) + ratebar.set_size_request(240, 15) + item.add(ratebar) + self.insert(item, -1) + + self.show_all() + + pitchadj.connect("value_changed", self.pitch_adjusted) + rateadj.connect("value_changed", self.rate_adjusted) + + def pitch_adjusted(self, widget): + """Cuando cambia pitch.""" + + self.pitch = int(widget.get_value()) + self.emit('pitch', self.pitch) + #self.face.say_notification(_("pitch adjusted")) + + def rate_adjusted(self, widget): + """Cuando cambia rate.""" + + self.rate = int(widget.get_value()) + self.emit('rate', self.rate) + #self.face.say_notification(_("rate adjusted")) + +class ToolbarFace(Gtk.Toolbar): + """Toolbar con opciones para la cara.""" + + __gsignals__ = {"number-eyes":(GObject.SIGNAL_RUN_FIRST, + GObject.TYPE_NONE, (GObject.TYPE_INT, )), + "switch-type-mouth":(GObject.SIGNAL_RUN_FIRST, + GObject.TYPE_NONE, (GObject.TYPE_STRING, )), + "switch-type-eyes":(GObject.SIGNAL_RUN_FIRST, + GObject.TYPE_NONE, (GObject.TYPE_STRING, ))} + + def __init__(self): + + Gtk.Toolbar.__init__(self) + self.modify_bg(0, Gdk.Color(0, 0, 0)) + + self.insert(get_separador(draw = False, ancho = 32, expand = False), -1) + + item = Gtk.ToolItem() + label = Gtk.Label("Mouth:") + label.modify_fg(0, Gdk.Color(65000, 65000, 65000)) + item.add(label) + self.insert(item, -1) + + self.insert(get_separador(draw = False, ancho = 5, expand = False), -1) + + item = Gtk.ToolItem() + combo = Gtk.ComboBoxText() + combo.append_text("Simple") + combo.append_text("Waveform") + combo.append_text("Frequency") + combo.set_active(0) + combo.connect('changed', self.emit_switch_type_mouth) + item.add(combo) + self.insert(item, -1) + + self.insert(get_separador(draw = False, ancho = 5, expand = False), -1) + + item = Gtk.ToolItem() + label = Gtk.Label("Eyes:") + label.modify_fg(0, Gdk.Color(65000, 65000, 65000)) + item.add(label) + self.insert(item, -1) + + self.insert(get_separador(draw = False, ancho = 5, expand = False), -1) + + item = Gtk.ToolItem() + combo = Gtk.ComboBoxText() + combo.append_text("Round") + combo.append_text("Glasses") + combo.set_active(0) + combo.connect('changed', self.emit_switch_type_eyes) + item.add(combo) + self.insert(item, -1) + + self.insert(get_separador(draw = False, ancho = 5, expand = False), -1) + + item = Gtk.ToolItem() + label = Gtk.Label("Eyes number:") + label.modify_fg(0, Gdk.Color(65000, 65000, 65000)) + item.add(label) + self.insert(item, -1) + + self.insert(get_separador(draw = False, ancho = 5, expand = False), -1) + + item = Gtk.ToolItem() + numeyesadj = Gtk.Adjustment(2, 1, 5, 1, 1, 0) + numeyesbar = Gtk.Scale(orientation=Gtk.Orientation.HORIZONTAL) + numeyesbar.set_adjustment(numeyesadj) + numeyesbar.set_draw_value(False) + numeyesbar.set_size_request(240, 15) + item.add(numeyesbar) + self.insert(item, -1) + + self.show_all() + + numeyesadj.connect("value_changed", self.eyes_changed_number) + + def emit_switch_type_mouth(self, widget): + """Cuando cambia el tipo de boca.""" + + self.emit("switch-type-mouth", widget.get_active_text()) + + def emit_switch_type_eyes(self, widget): + """Cuando cambia el tipo de ojos.""" + + self.emit("switch-type-eyes", widget.get_active_text()) + + def eyes_changed_number(self, widget): + """Cuando se cambia la cantidad de ojos.""" + + self.emit('number-eyes', int(widget.get_value())) + #self.face.say_notification(_("eyes changed")) + \ No newline at end of file diff --git a/activity/activity-speak.svg b/activity/activity-speak.svg new file mode 100644 index 0000000..82d12a5 --- /dev/null +++ b/activity/activity-speak.svg @@ -0,0 +1,14 @@ + + + +]> + + + + + + + + diff --git a/activity/activity.info b/activity/activity.info new file mode 100644 index 0000000..d39ea5f --- /dev/null +++ b/activity/activity.info @@ -0,0 +1,23 @@ +[Activity] +implement = speak +name = Speak +summary = An animated face that speaks whatever you type +description = Speak is a talking face for the XO laptop. Anything you type will + be spoken aloud using the XO's speech synthesizer, espeak. You can + adjust the accent, rate and pitch of the voice as well as the shape + of the eyes and mouth. + This is a great way to experiment with the speech synthesizer, + learn to type or just have fun making a funny face for your XO. +homepage = http://wiki.sugarlabs.org/go/Activities/Speak +license = GPLv3+ + +version = 41 +stability = stable + +icon = activity-speak +exec = sugar-activity Speak.Speak + +# original activity.info options +activity_version = 41 +bundle_id = vu.lux.olpc.Speak + diff --git a/face.py b/face.py new file mode 100644 index 0000000..0a32781 --- /dev/null +++ b/face.py @@ -0,0 +1,438 @@ +#!/usr/bin/env python +# -*- 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 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 math + +import gi +from gi.repository import Gtk +from gi.repository import Gdk + +class Face(Gtk.EventBox): + """La cara.""" + + def __init__(self): + + Gtk.EventBox.__init__(self) + + self.eyes_box = None + self.mouth = None + + self.type_eyes = 'Round' + self.type_mouth = 'Simple' + + self.eyes_box = Gtk.Box(orientation=Gtk.Orientation.HORIZONTAL) + mouthbox = Gtk.Box(orientation=Gtk.Orientation.HORIZONTAL) + + box = Gtk.Box(orientation=Gtk.Orientation.VERTICAL) + box.pack_start(self.eyes_box, True, True, 0) + box.pack_start(mouthbox, True, True, 0) + + self.mouth = Mouth() + mouthbox.pack_start(self.mouth, True, True, 0) + + self.add(box) + + self.set_number_eyes(2) + + self.show_all() + + def set_type_mouth(self, tipo): + """Actualizar el tipo de boca. + tipo puede ser 'Simple' o 'Waveform' o 'Frequency'""" + + self.type_mouth = tipo + self.mouth.tipo = self.type_mouth + + def set_type_eyes(self, tipo): + """Actualizar el tipo de ojos. + tipo puede ser 'Round' o 'Glasses'""" + + self.type_eyes = tipo + for child in self.eyes_box.get_children(): + child.set_type(self.type_eyes) + + def set_number_eyes(self, number): + """Actualizar la cantidad de ojos.""" + + while len(self.eyes_box.get_children()) < number: + self.add_eye() + + while len(self.eyes_box.get_children()) > number: + self.del_eye() + + self.set_type_eyes(self.type_eyes) + + def add_eye(self): + """Agrega un ojo.""" + + self.eyes_box.pack_start(Eye(), True, True, 0) + + def del_eye(self): + """Quita un ojo.""" + + childs = self.eyes_box.get_children() + if childs: + eye = childs[-1] + self.eyes_box.remove(eye) + eye.destroy() + + def look_at(self, pos): + """Mirar hacia pos""" + + x, y = pos + map(lambda e, x=x, y=y: e.look_at(x, y), self.eyes_box.get_children()) + + +class Eye(Gtk.DrawingArea): + """El ojo. + self.tipo puede ser 'Round' o 'Glasses'.""" + + def __init__(self): + + Gtk.DrawingArea.__init__(self) + + self.x, self.y = 0, 0 + self.fill_color = (49000, 52000, 18000) + self.tipo = 'Round' + + self.show_all() + + def set_type(self, tipo): + """Cambia el tipo de ojo. + tipo puede ser 'Round' o 'Glasses'""" + + self.tipo = tipo + self.queue_draw() + + def look_at(self, x, y): + """ Look. . .""" + + self.x = x + self.y = y + self.queue_draw() + + def computePupil(self): + """La Pupila.""" + + rect = self.get_allocation() + + if self.x is None or self.y is None: + if rect.x + rect.width / 2 < rect.width / 2: + cx = rect.width * 0.6 + else: + cx = rect.width * 0.4 + return cx, rect.height * 0.6 + + EYE_X, EYE_Y = self.translate_coordinates( + self.get_toplevel(), rect.width / 2, rect.height / 2) + EYE_HWIDTH = rect.width + EYE_HHEIGHT = rect.height + BALL_DIST = EYE_HWIDTH / 4 + + dx = self.x - EYE_X + dy = self.y - EYE_Y + + if dx or dy: + angle = math.atan2(dy, dx) + cosa = math.cos(angle) + sina = math.sin(angle) + h = math.hypot(EYE_HHEIGHT * cosa, EYE_HWIDTH * sina) + x = (EYE_HWIDTH * EYE_HHEIGHT) * cosa / h + y = (EYE_HWIDTH * EYE_HHEIGHT) * sina / h + dist = BALL_DIST * math.hypot(x, y) + + if dist < math.hypot(dx, dy): + dx = dist * cosa + dy = dist * sina + + return rect.width / 2 + dx, rect.height / 2 + dy + + def do_draw(self, context): + rect = self.get_allocation() + eyeSize = min(rect.width, rect.height) + + outlineWidth = eyeSize / 20.0 + pupilSize = eyeSize / 10.0 + pupilX, pupilY = self.computePupil() + dX = pupilX - rect.width / 2. + dY = pupilY - rect.height / 2. + distance = math.sqrt(dX * dX + dY * dY) + limit = eyeSize / 2 - outlineWidth * 2 - pupilSize + if distance > limit: + pupilX = rect.width / 2 + dX * limit / distance + pupilY = rect.height / 2 + dY * limit / distance + + r,g,b = self.fill_color + context.set_source_rgba(r,g,b,1) + context.rectangle(0, 0, rect.width, rect.height) + context.fill() + + if self.tipo == 'Round': + # eye ball + context.set_source_rgb(1, 1, 1) + context.arc(rect.width / 2, + rect.height / 2, + eyeSize / 2 - outlineWidth / 2, + 0, 360) + context.fill() + + # outline + context.set_source_rgb(0, 0, 0) + context.set_line_width(outlineWidth) + context.arc(rect.width / 2, + rect.height / 2, + eyeSize / 2 - outlineWidth / 2, + 0, 360) + context.stroke() + + elif self.tipo == 'Glasses': + def roundrect(x1, y1, x2, y2): + context.move_to(x1, (y1 + y2) / 2.) + context.curve_to(x1, y1, x1, y1, (x1 + x2) / 2., y1) + context.curve_to(x2, y1, x2, y1, x2, (y1 + y2) / 2.) + context.curve_to(x2, y2, x2, y2, (x1 + x2) / 2., y2) + context.curve_to(x1, y2, x1, y2, x1, (y1 + y2) / 2.) + + # eye ball + context.set_source_rgb(1, 1, 1) + roundrect(outlineWidth, + outlineWidth, + rect.width - outlineWidth, + rect.height - outlineWidth) + context.fill() + + # outline + context.set_source_rgb(0, 0, 0) + context.set_line_width(outlineWidth) + roundrect(outlineWidth, + outlineWidth, + rect.width - outlineWidth, + rect.height - outlineWidth) + context.stroke() + + # pupil + context.arc(pupilX, pupilY, pupilSize, 0, 360) + context.set_source_rgb(0, 0, 0) + context.fill() + + return True + +class Mouth(Gtk.DrawingArea): + """La boca.""" + + def __init__(self): + + Gtk.DrawingArea.__init__(self) + + self.fill_color = (49000, 52000, 18000) + self.volume = 0 + self.tipo = "Simple" + + self.show_all() + + def do_draw(self, context): + + rect = self.get_allocation() + + #self.processBuffer() + + # background + r,g,b = self.fill_color + context.set_source_rgba(r,g,b,1) + context.paint() + + # Draw the mouth + volume = self.volume / 30000. + mouthH = volume * rect.height + mouthW = volume ** 2 * (rect.width / 2.) + rect.width / 2. + # T + # L R + # B + Lx, Ly = rect.width / 2 - mouthW / 2, rect.height / 2 + Tx, Ty = rect.width / 2, rect.height / 2 - mouthH / 2 + Rx, Ry = rect.width / 2 + mouthW / 2, rect.height / 2 + Bx, By = rect.width / 2, rect.height / 2 + mouthH / 2 + context.set_line_width(min(rect.height / 10.0, 10)) + context.move_to(Lx, Ly) + context.curve_to(Tx, Ty, Tx, Ty, Rx, Ry) + context.curve_to(Bx, By, Bx, By, Lx, Ly) + context.set_source_rgb(0, 0, 0) + context.close_path() + context.stroke() + + return True + + ''' + def _new_buffer(self, obj, buf): + if len(buf) < 28: + self.newest_buffer = [] + else: + self.newest_buffer = list(unpack(str(int(len(buf)) / 2) + 'h', + buf)) + self.main_buffers += self.newest_buffer + if(len(self.main_buffers) > self.buffer_size): + del self.main_buffers[0:(len(self.main_buffers) - \ + self.buffer_size)] + + self.queue_draw() + return True + + def processBuffer(self): + if len(self.main_buffers) == 0 or len(self.newest_buffer) == 0: + self.volume = 0 + else: + self.volume = numpy.core.max(self.main_buffers) # -\ + # numpy.core.min(self.main_buffers) + + + + +class WaveformMouth(Mouth): + def __init__(self, audioSource, fill_color): + + Mouth.__init__(self, audioSource, fill_color) + + self.buffer_size = 100 + self.peaks = [] + + self.stop = False + + self.y_mag_bias_multiplier = 1 + self.y_mag = 0.7 + + self.show_all() + + def draw_wave(self, context): + rect = self.get_allocation() + self.param1 = rect.height / 65536.0 + self.param2 = rect.height / 2.0 + + # background + context.set_source_rgba(*self.fill_color.get_rgba()) + context.paint() + + # Draw the waveform + context.set_line_width(min(rect.height / 10.0, 10)) + count = 0 + buflen = float(len(self.main_buffers)) + for value in self.main_buffers: + peak = float(self.param1 * value * self.y_mag) +\ + self.y_mag_bias_multiplier * self.param2 + + if peak >= rect.height: + peak = rect.height + if peak <= 0: + peak = 0 + + x = count / buflen * rect.width + context.line_to(x, rect.height - peak) + + count += 1 + context.set_source_rgb(0, 0, 0) + context.stroke() + + return True + + +class FFTMouth(Mouth): + def __init__(self, audioSource, fill_color): + + Mouth.__init__(self, audioSource, fill_color) + + self.peaks = [] + + self.y_mag = 1.7 + self.freq_range = 70 + self.draw_interval = 1 + self.num_of_points = 105 + + self.stop = False + + #constant to multiply with self.param2 while scaling values + self.y_mag_bias_multiplier = 1 + + self.fftx = [] + + self.scaleX = "10" + self.scaleY = "10" + + self.show_all() + self.connect('draw', self.draw_fftmouth) + + def newprocessBuffer(self, rect): + self.param1 = rect.height / 65536.0 + self.param2 = rect.height / 2.0 + + if(self.stop == False): + + Fs = 48000 + nfft = 65536 + self.newest_buffer = self.newest_buffer[0:256] + self.fftx = fft(self.newest_buffer, 256, -1) + + self.fftx = self.fftx[0:self.freq_range * 2] + self.draw_interval = rect.width / (self.freq_range * 2.) + + NumUniquePts = ceil((nfft + 1) / 2) + self.buffers = abs(self.fftx) * 0.02 + self.y_mag_bias_multiplier = 0.1 + self.scaleX = "hz" + self.scaleY = "" + + if(len(self.buffers) == 0): + return False + + # Scaling the values + val = [] + for i in self.buffers: + temp_val_float = float(self.param1 * i * self.y_mag) +\ + self.y_mag_bias_multiplier * self.param2 + + if(temp_val_float >= rect.height): + temp_val_float = rect.height - 25 + if(temp_val_float <= 0): + temp_val_float = 25 + val.append(temp_val_float) + + self.peaks = val + + def draw_fftmouth(self, widget, context): + rect = widget.get_allocation() + + self.newprocessBuffer(rect) + + # background + context.set_source_rgba(*self.fill_color.get_rgba()) + context.paint() + + # Draw the waveform + context.set_line_width(min(rect.height / 10.0, 10)) + context.set_source_rgb(0, 0, 0) + count = 0 + for peak in self.peaks: + context.line_to(rect.width / 2 + count, + rect.height / 2 - peak) + count += self.draw_interval + context.stroke() + count = 0 + for peak in self.peaks: + context.line_to(rect.width / 2 - count, + rect.height / 2 - peak) + count += self.draw_interval + context.stroke() + + return True''' \ No newline at end of file diff --git a/icons/edit-description.svg b/icons/edit-description.svg new file mode 100644 index 0000000..cf59acc --- /dev/null +++ b/icons/edit-description.svg @@ -0,0 +1,14 @@ + + + +]> + + + + + + + + diff --git a/icons/face.svg b/icons/face.svg new file mode 100644 index 0000000..6e2503f --- /dev/null +++ b/icons/face.svg @@ -0,0 +1,103 @@ + + + + + + image/svg+xml + + + + + + + + + + + + + + diff --git a/icons/mode-chat.svg b/icons/mode-chat.svg new file mode 100644 index 0000000..9b38013 --- /dev/null +++ b/icons/mode-chat.svg @@ -0,0 +1,128 @@ + + + + + + image/svg+xml + + + + + + + + + + + + + + + + diff --git a/icons/mode-robot.svg b/icons/mode-robot.svg new file mode 100644 index 0000000..420e922 --- /dev/null +++ b/icons/mode-robot.svg @@ -0,0 +1,119 @@ + + + +image/svg+xml + + \ No newline at end of file diff --git a/icons/mode-type.svg b/icons/mode-type.svg new file mode 100644 index 0000000..ddf7dbc --- /dev/null +++ b/icons/mode-type.svg @@ -0,0 +1,82 @@ + + + +image/svg+xml + +ABC + \ No newline at end of file diff --git a/icons/voice.svg b/icons/voice.svg new file mode 100644 index 0000000..d32f370 --- /dev/null +++ b/icons/voice.svg @@ -0,0 +1,66 @@ + +image/svg+xml + + + + + + \ No newline at end of file diff --git a/setup.py b/setup.py new file mode 100644 index 0000000..5cae440 --- /dev/null +++ b/setup.py @@ -0,0 +1,27 @@ +#!/usr/bin/env python + +# Speak.activity +# A simple front end to the espeak text-to-speech engine on the XO laptop +# http://wiki.laptop.org/go/Speak +# +# Copyright (C) 2008 Joshua Minor +# This file is part of Speak.activity +# +# Parts of Speak.activity are based on code from Measure.activity +# Copyright (C) 2007 Arjun Sarwal - arjun@laptop.org +# +# Speak.activity 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. +# +# Speak.activity 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 Speak.activity. If not, see . + +from sugar3.activity import bundlebuilder +bundlebuilder.start() -- cgit v0.9.1