Archivo por años: 2012

Subtitulando

Estos días estoy liado buscando alternativas a periscope para bajar subtítulos de manera automática. El motivo es que no es un proyecto que esté siendo mantenido de manera razonable.

De casualidad encontré subliminal, un programa similar pero con más fuentes de subtítulos, y decidí echarle un ojo. La verdad es que funciona muy bien, pero tiene el inconveniente de que, al igual que periscope, está escrito en python, lo que significa que hay que meter una máquina virtual completa en mi WebTV.

Pero de casualidad encontré submarine, otro programa similar pero escrito en Vala, lo que significa que puedo compilarlo en un ejecutable y ahorrarme todo el interprete de python. Por desgracia, este sólo soporta búsquedas en OpenSubtitles.org y en Podnapisi.net. Sin embargo, su modelo interno es tan sumamente sencillo que he conseguido añadir soporte para SubDB (el cual ya está subido a mi repositorio GIT personal y solicitado un commit al oficial).

Extraoficialmente también he conseguido implementar soporte para BierDopje, pero ese código todavía no lo puedo subir porque sigo a la espera de que me concedan una clave específica para la API (en estos momentos estoy usando prestada la de subliminal, pero cada aplicación tiene que usar la suya propia).

El uso de este portal tiene su complicación, en buena medida porque mientras que SubDB utiliza un hash del archivo para buscar subtítulos, BierDopje pide el nombre de la serie, la temporada y el capítulo. En subliminal se utiliza la impresionante biblioteca guessit, que, a partir del nombre del fichero, y mediante una serie de heurísticos, extrae toda la información posible sobre un fichero. Obviamente dicha biblioteca está escrita en python, así que tuve que hacer una versión de andar por casa en Vala. El concepto básico es el mismo (de hecho, fusilé todo lo que pude), pero, al estar hecha en una noche, no es tan precisa. Obviamente acepto parches para mejorarla.

Para jugar bien

En la entrada anterior explicaba el problema que supone jugar cuando se utiliza un gestor de ventanas por composición. Este problema se describe con más detalle en los artículos The Cost Of Running CompizMutter Can Cause A Gaming/OpenGL Performance Hit Too. El principal motivo es que los programas no pintan directamente sobre la pantalla, sino sobre un buffer oculto, y es el gestor de ventanas el que, de manera periódica, copia dicho buffer a la zona visible.

La solución que proponía era aumentar la prioridad del gestor de ventanas y del servidor X, pero dado que es una operación algo liosa para alguien que no tenga unos mínimos conocimientos, decidí hacer un pequeño programa que lo automatizase. Ese programa se llama GAMEd. La esencia es muy sencilla: tenemos una lista de ejecutables almacenada en /etc/gamed.conf. Cada vez que se lanza el gestor de ventanas y el entorno de escritorio, se llama mediante DBus al demonio GAMEd (y si no estaba lanzado, el propio DBus lo lanza como root). Este demonio recorre la lista de procesos actuales, y cambia su prioridad a cada uno que coincida con alguno de la lista. De esta manera es posible asignar prioridades altas desde el espacio de usuario sin abrir un agujero de seguridad, porque sólo se cambiarán aquellos programas autorizados.

El sistema está diseñado para ser completamente transparente: por defecto asigna prioridad -15, pero se puede utilizar el comando renice_gamed para escoger cualquier otra (podría llegarse hasta -20, pero en el código de un programa similar, AutoNICEd, recomienda no hacerlo, así que GAMEd simplemente redondea a -15 si se pone una prioridad mayor). Dicha prioridad se almacena y se utiliza todas las veces que se llame al demonio, hasta que el usuario la vuelva a cambiar si quiere.

Por otro lado se añade un pequeño script en el arranque del escritorio, que llama al demonio. Esto hace que cada vez que se arranque el ordenador y se entre en la sesión, se ajustará de nuevo automáticamente la prioridad de las X y del gestor de ventanas.

Yo lo he probado en Gala y en Compiz y se nota la diferencia, pero no he podido probarlo en Gnome-Shell por ciertos problemas de compatibilidad de paquetes entre éste y Elementary OS.

Así pues, si jugáis regularmente en vuestro equipo Linux y notáis pérdida de rendimiento y de FPS en vuestros juegos, probad a instalar GAMEd y contadme qué tal os ha ido.

Juego elemental

Llevo una temporadita probando Elementary OS, y tengo que decir que me encanta. Es un concepto de escritorio y sistema operativo que, salvo un par de detalles, encaja como un guante en lo que busco. Tanto es así que estoy asegurándome de que Cronopete esté perfectamente integrado en él.

Por otro lado, hace poco descubrí Limbo, un juego con una ambientación sencillamente impresionante.

Sin embargo, me encontré con que la mezcla de ambos no funcionaba muy bien: había «saltos» entre fotogramas, se conseguían muy pocos FPS y había un retardo brutal (casi un segundo) entre la pulsación de una tecla y que el personaje ejecutase la acción correspondiente. Al principio lo achaqué a Wine (es un juego para windows), pero buscando información encontré que al resto de gente le funcionaba perfectamente.

Lo primero que hice fue cambiar del driver privativo de AMD/ATI (FGLRX) al libre (Radeon), y los saltos desaparecieron, pero los bajos FPS y el retardo seguían allí.

Recordé que había una crítica general contra los gestores de ventanas por composición como Compiz, Mutter y KWin, porque tienen que renderizar el contenido de cada ventana cada vez que la aplicación lo cambia. Ante esto, probé a arrancar una sesión de gnome-fallback-session, y allí funcionaba perfectamente, lo que confirmó que el problema estaba en Gala (el gestor de ventanas de Elementary).

Sin embargo, pensando, llegué a la conclusión de que perfectamente podía deberse al retardo en la conmutación entre tareas, así que decidí probar a asignarle máxima prioridad (-19) tanto al servidor X como a Gala. Para ello usé el comando renice (como root, porque si no, no permite asignar valores negativos, correspondientes a la máxima prioridad). Con eso el problema se resolvió, y el juego funcionó con una suavidad total, igual que si no estuviese utilizando un gestor de ventanas por composición.

PPAs

Tras arduos intentos, por fin he conseguido crear un repositorio PPA para Cronopete. Gracias a él, los usuarios de Debian, Ubuntu y Elementary OS tendrán mucho más fácil instalarlo. También gracias a esto he conseguido pulir algunos problemillas en los scripts de CMake, para que funcione al 100% la instalación en directorios no estándar.

En cronopete en sí casi no hay cambios, salvo haber añadido soporte de D-Bus activation, para que si no está lanzado, y el usuario hace click en el icono de configuración o de restauración de archivos, se lance automáticamente.

Lo que no he conseguido aún es que, tras instalar el paquete, el sistema pida al usuario reiniciar (y que así se lance Cronopete en el siguiente arranque).

¡A disfrutarla!

Lavado de cara

Después de unos cuantos años con la misma apariencia, y varios comentarios de otra gente, me decidí a cambiar un poco el diseño de mi web. No es que sea un cambio radical, pero al menos está más limpio.

Por otro lado acabo de lanzar la versión 3.4.0 de Cronopete (sí, he decidido usar números de sub-versión impares para las versiones de desarrollo), con varias novedades internas. La principal es que ahora utilizo CMake para compilarlo, por lo que será más fácil que terceros hagan paquetes DEB o RPM. Por otro lado, he simplificado los mensajes y retocado los gráficos, además de portarlo a appindicator. Todo esto con el objetivo de que sea incorporado a Elementary OS en un futuro cercano (o lo que es lo mismo: en la versión siguiente a Luna).

Que la disfrutéis con salud.

Funciones asíncronas en Vala

Actualizado. Acabo de modificar un poco el código de Cronopete para que haga uso de las funciones asíncronas de Vala. El motivo es eliminar, en la medida de lo posible, el uso de threads.

Las funciones asíncronas de Vala son una ayuda muy interesante a la hora de realizar tareas que requieren mucho tiempo, a la vez que se quiere evitar que la interfaz se bloquee. La solución más inmediata en estos casos consiste en crear un thread y realizar en él todo el proceso, pero en ocasiones es una solución muy compleja, pues se necesita intercambiar datos entre éste y el thread principal, o sincronizar ambos. La solución está en las funciones asíncronas. Estas son funciones que pueden interrumpirse y reanudarse a voluntad en cualquier punto, lo que permite realizar una forma de multitarea cooperativa de manera sencilla.

Imaginemos que definimos una función o método asíncronos así:

public async int AsyncFnc(int p1, int p2, out int p3, out String p4) {
    // código de la función asíncrona (también puede ser un método de una clase)
    [...]
}

Para llamar a esta función, lo haremos de esta manera:

AsyncFnc.begin(p1, p2, callback_finalizacion);

Vemos que sólo incluimos los parámetros de entrada, pero no los de salida. Por otro lado, tampoco recogemos el valor devuelto. Por último, tenemos un misterioso callback_finalizacion. Este parámetro es opcional, y sólo hay que añadirlo cuando se desea ejecutar algún código cuando la función asíncrona finalice. Es en dicha función donde recogeremos los valores de salida y cualquier excepción que se produjese durante la ejecución de la función asíncrona. Dicho callback es de tipo GLib.AsyncReadyCallback, y su formato es el siguiente:

public delegate void AsyncReadyCallback (Object? source_object, AsyncResult res)

Sin embargo, Vala nos ofrece algunas ventajas, como son las funciones anónimas y los closures, que nos dan como ventaja el poder acceder desde el callback a variables externas a éstas. Así pues, utilizando una función anónima podemos llamar a nuestra función asíncrona así:

AsyncFnc.begin(p1, p2, (obj,res) => {

    int retval;
    retval=AsyncFnc.end(res, out p3, out p4);

    // resto del código del callback
    [...]
});

De esta manera, cuando la función asíncrona llegue al final y retorne, se ejecutará el código de callback contenido en el closure. Vemos también que llamamos a AsyncFnc.end; esta es, precisamente, la manera que tenemos de obtener el valor devuelto por la función asíncrona, así como los parámetros de salida. También es esta llamada la que debemos rodear con un try/catch() si queremos capturar cualquier excepción lanzada desde dentro de la función asíncrona.

Así pues, ya sabemos llamar a una función asíncrona, y sabemos como recibir sus resultados. ¿Pero como hacemos para que ceda el control y devolvérselo? No olvidemos que NO se tratan de varios threads paralelos, sino de un único hilo de ejecución.

La respuesta está en yield y .callback(). La primera es una llamada que, al realizarla dentro de una función asíncrona, devuelve el control a la función llamante. La segunda es un método de la función asíncrona (igual que .begin y .end; podemos comparar una función asíncrona con un objeto que ofrece esos tres métodos públicos) que lo que hace es continuar la ejecución de dicha función justo después del último yield ejecutado. Un detalle interesante es que las llamadas a yield se pueden situar absolutamente en cualquier parte de una función asíncrona, incluso dentro de un bucle, lo que nos permite, de una manera sencilla, devolver el control en cada iteración para que se vayan ejecutando otras partes del código, y recuperarlo para la siguiente iteración.

Ahora que ya tenemos la imagen completa podemos ver como se ejecuta el código: cuando una función normal llama a una función asíncrona, le cede el control y ésta comienza a ejecutarse hasta alcanzar el primer yield. En ese punto la llamada retorna a la función normal, y sigue ejecutándose el código situado a continuación de la llamada. Cuando, en algún punto posterior, se llame al .callback() de la función asíncrona, el control pasará al código situado justo después de dicho primer yield, y seguirá ejecutándose hasta que encuentre otro yield, momento en que el control de la ejecución pasará al código situado justo después de la llamada a .callback(). Este juego de ping-pong continuará hasta que la función asíncrona llegue al final y retorne. En ese momento se ejecutará el código del callback y continuará la ejecución después de la última llamada a .callback().

Un ejemplo gráfico

Para entender mejor este proceso, veámoslo en forma gráfica. Por cuestiones de claridad lo dividiremos en dos imágenes: en la primera veremos el flujo desde que se inicia la función asíncrona hasta que está a punto de acabar, y en la segunda desde que se ejecuta el return al final.

En la imagen anterior vemos la primera parte del ciclo de vida de una función asíncrona. Cada función está representada por un rectángulo, y dentro de ella, el flujo de ejecución natural es de arriba a abajo.

Desde el Main Loop de la aplicación se llama a un callback (por ejemplo, porque el usuario ha pulsado un botón en la interfaz gráfica), llamado Function en este ejemplo, y en éste se llama a nuestra función asíncrona (llamada AsyncFnc en el ejemplo), que comienza a ejecutarse. En el momento en el que se alcanza el primer yield, el control vuelve al callback que la lanzó, que sigue su ejecución hasta que, en un punto, éste llama a AsyncFnc.callback(). En ese momento el flujo de ejecución vuelve a la función asíncrona, que continúa justo en la instrucción que sigue al primer yield. Un poco después se llega a un segundo yield, que devuelve el control al callback original.

Cuando el callback termina, se devuelve el control al Main Loop, como siempre. Pero la función asíncrona aún no ha terminado. Vemos entonces que en dos puntos determinados se llama a AsyncFnc.callback(). Estas llamadas no son automáticas, sino que tuvieron que ser preprogramadas (por ejemplo con un temporizador, o con una llamada a Idle.add(AsyncFnc.callback) para que se ejecuten en un momento en que no hay eventos por despachar). Cada vez que se llama a dicha función, la ejecución de la función asíncrona continúa justo después del último yield, hasta que encuentra otro, o bien hasta que finaliza la función.

En el siguiente esquema, las flechas en rojo muestran el flujo de ejecución cuando la función asíncrona llega al final: en ese momento se ejecuta el callback registrado cuando se lanzó la función (si es que existe, claro; si no se definió, se retorna directamente), y cuando se termina éste, se retorna al punto de llamada.

Llamar a una función asíncrona desde otra función asíncrona

A la hora de llamar a una función asíncrona desde otra función asíncrona, la primera idea que nos puede venir a la cabeza es hacer

// ATENCION, ESTE CODIGO NO FUNCIONARA CORRECTAMENTE

async void AsyncFnc1() {
    int retval;

    [...]
    AsyncFnc2( (ob,res) => {
        retval=AsyncFnc2.end();
        AsyncFnc1.callback();
    });
    yield;
    [...]
}

Aparentemente, este código lanzaría AsyncFnc2 y suspendería la ejecución de AsyncFnc1, pero de manera que, al terminarse la ejecución de AsyncFnc2, continuaría la ejecución de AsyncFnc1.

Por desgracia, ese código es probable que no funcione correctamente, como mínimo en un caso concreto: si, por la razón que sea, la función AsyncFnc2 finaliza sin llamar a yield al menos una vez, se llamará antes a AsyncFnc1.callback() que al yield, por lo que el código fallará. Una solución a este problema sería utilizar Idle.add(AsyncFnc1.callback) en su lugar, pero sólo funcionará si tenemos un Main Loop en nuestro programa, pues será éste quien llame al callback después de que la función asíncrona retorne tras ejecutar el yield.

Afortunadamente, los diseñadores de Vala eran conscientes de que es muy común llamar a una función asíncrona desde otra, por lo que simplificaron notablemente este caso concreto. Así, para llamar a AsyncFnc2 desde AsyncFnc1, de manera que ésta última continúe su ejecución cuando acabe la otra, sólo hay que hacer

async void AsyncFnc1() {
    int retval;

    [...]
    retval=yield AsyncFnc2();
    [...]
}

Otra ventaja es que, en este caso, no necesitamos llamar a AsyncFnc2.end() para obtener los resultados, porque se hace directamente.

El flujo de ejecución sería así:

Vemos que desde el Main Loop llamamos a nuestra primera función asíncrona, AsyncFnc1, y después de un trozo de código llamamos, mediante yield, a nuestra segunda función asíncrona, AsyncFnc2. En ésta ejecutamos algo de código y llegamos a un yield. Sin embargo, éste no retorna a AsyncFnc1, sino a la llamada anterior. Esto es así porque AsyncFnc1 lo que quiere es dormir hasta que AsyncFnc2 termine.

También vemos que para reanudar la ejecución es preciso llamar a AsyncFnc2.callback(). Esto debe tenerse muy en cuenta: en este instante es incorrecto llamar a AsyncFnc1.callback(). Por eso es fundamental que cada función asíncrona se encargue de programar su llamada para recuperar el control, y no delegarlo a funciones externas, pues es muy fácil cometer errores.

Cuando AsyncFnc2 llega al final, se cede el control al código de AsyncFnc1, que sigue ejecutándose, y al terminar, se llamaría a la función de callback de finalización (si la hubiera), y seguiría la ejecución justo a continuación de la llamada a AsyncFnc2.callback() (que, en este caso, simplemente seguiría con la ejecución del Main Loop).

Casos de uso de funciones asíncronas

El primer caso de uso de las funciones asíncronas es para llamar a otras funciones asíncronas. Esto, que puede parecer una perogrullada, toma sentido en cuanto descubrimos las funciones asíncronas de Gio. Un ejemplo es el de File.copy_async: este método de la clase File copia un fichero de manera asíncrona. De no existir, tendríamos que crear un hilo y ejecutar en él la función síncrona de copia, porque de no hacerlo así, si el fichero fuese muy grande la interfaz gráfica de nuestro programa se quedaría congelada durante la operación.

Hay dos maneras de llamar a estas funciones asíncronas: la primera es la clásica, incluyendo un callback (bien como puntero a función, bien como función anónima o closure) que se llame cuando termine la operación; la segunda, más cómoda en según que casos, es llamarla mediante yield desde una función asíncrona de Vala. En este segundo caso hay que saltarse el parámetro del callback.

El segundo caso de uso es para llamar a servicios externos mediante DBus. De esta manera podemos aprovechar el tiempo entre el envío de la petición y la llegada de la respuesta. Para ello, basta con añadir async en la definición de la interfaz DBus. Un ejemplo de como definir la interfaz DBus para formatear un disco, mediante el demonio UDisks:

[DBus (name = "org.freedesktop.UDisks.Device")]
interface Device_if : GLib.Object {
	public abstract async void FilesystemCreate(string type, string[] options) throws IOError;
}

Por desgracia, en este caso concreto no podemos aprovechar la funcionalidad de la llamada mediante yield, debido a que existe un timeout en las llamadas a métodos DBus de, aproximadamente, 25 segundos, y se necesita más tiempo para formatear algunos soportes. Se supone que se puede ajustar este timeout mediante un modificador de Vala, pero después de consultar en la lista de correo y de buscar en internet, no he conseguido que funcione. Si alguna alma caritativa nos puede iluminar…

El siguiente caso de uso es cuando se quiere realizar una tarea larga y que consuma CPU, pero no se quieren usar threads o lanzar un proceso paralelo, sino ejecutarlo en los momentos en los que el proceso actual esté inactivo (en el caso de una aplicación gráfica, cuando no hay eventos que procesar en la cola). La solución consiste en crear una función asíncrona que realice la tarea, insertando llamadas a yield de manera regular. Para que la función se despierte cuando la cola está vacía, usamos Idle.add() para llamar al método .callback() de nuestra función asíncrona, así:

public async void AsyncFnc() {

    // Inicialización de variables y demás
    [...]

    // Comienza la tarea
    while(cond==true) {

        // Realizamos una iteración de la tarea
        [....]

        // Avisamos al Main Loop que queremos que nos despierte
        // cuando no haya eventos pendientes
        Idle.add(AsyncFnc.callback);

        // Y devolvemos el control al búcle principal o Main Loop
        yield;
    }

    // La tarea ha finalizado
    // Limpiamos todo
    [...]
}

Por supuesto, podemos utilizar también un temporizador en lugar de Idle, o cualquier otro sistema que decidamos.

Un detalle interesante que se puede ver en este ejemplo es que la llamada a yield puede ir dentro de un bucle while o for sin ningún problema.

Por último, también son útiles para sincronizar threads. El truco en este caso está en almacenar un puntero al método de callback de la función asíncrona que queremos sincronizar, lanzar el thread, y al final de éste, añadir dicho callback a la cola de mensajes. El esquema es éste:

async void AsyncFunc() {

    // Aquí almacenamos el método .callback() de nuestra función asíncrona
    SourceFunc callback = AsyncFunc.callback;

    // Creamos la función para el thread. La hacemos anónima (closure) para que tenga acceso a la
    // variable local callback.
    ThreadFunc<void*> run = () => {
        // Código del thread. Aquí hacemos las operaciones deseadas
        [...]

        // Aquí ya hemos terminado las operaciones, así que
        // añadimos el método .callback() a la cola para que se llame inmediatamente
        Idle.add((owned) callback);
        return null;
    };

    // Lanzamos el thread
    Thread.create<void*>(run, false);

    // Y esperamos a que se llame a nuestra función callback
    // que será cuando termine el thread
    yield;
}

En la documentación oficial se encuentran tres ejemplos de funciones asíncronas, muy útiles para entender aún mejor esta útil característica de Vala. En concreto aparece un ejemplo del método de sincronización de threads, donde se puede ver mucho mejor la sintaxis concreta.

Usuarios perdidos

Acabo de descubrir que durante la migración del servidor se han perdido los usuarios registrados, así que tendréis que volver a daros de alta para poder hacer comentarios (exigir registro es la única manera de reducir el spam a niveles manejables).

Actualización: la opción de permitir darse de alta estaba desactivada, por lo que era imposible dejar comentarios. Acabo de corregirlo.

Entrada de línea en mi C2

Cuando, hace años, me compré mi Citroen C2, me conformé con el autorradio que traía de serie: un modelo relativamente sencillo de la marca Clarion. Este equipo trae radio y lector de CDs, además de permitir la conexión de un cargador de CDs externo. Por desgracia, sólo admite CDs normales, nada de MP3, por lo que es bastante limitado.

En varias ocasiones sopesé el cambiarlo por otro modelo, pero eso me obligaba a renunciar a los controles de volumen en el volante, o bien a comprar un (caro) cable adaptador. Encima, la entrada de cargador externo de CDs tienen que activarla en un concesionario para que funcione, por lo que conectar un reproductor externo de manera directa parecía una misión imposible.

Durante un tiempo utilicé un emisor de FM, pero el resultado era bastante pobre, en buena medida porque no conseguía encontrar una frecuencia que se mantuviese libre desde mi casa hasta el trabajo.

Todo parecía perdido cuando, hace unos días, encontré una entrada de Panito69 en el foro del Citroen C2 donde explica como añadir una entrada de línea a esta radio. Básicamente hay que conectarla en los puntos que se ven en esta foto que el propio Panito69 publica en dicha entrada (pincha en ella para agrandarla):

Una vez hecho, basta con meter un CD de música, y el sonido de lo que metamos por la entrada sonará en los altavoces. Por alguna misteriosa razón, al conectar cualquier dispositivo, el lector de CDs quedará silenciado.

En mi coche monté directamente un cable con un jack macho al extremo, para no tener que andar con más cables y demás. Este es el resultado con el móvil conectado, y con el cable recogido:

Sin embargo, el depender de un CD supone algunos problemillas: para empezar, el sonido de éste se escucha muy levemente entre canciones; por otro lado, cuando el CD se acaba y la radio vuelve al principio, el sonido se corta durante dos segundos, el tiempo que el cabezal tarda en volver al principio.

La solución que encontré fue crear un CD en blanco con la duración máxima posible. Con él, puedo escuchar el máximo tiempo posible la música, y con el mínimo de interferencias. Aquí podéis bajar el fichero WAV de 79 minutos que utilicé, listo para grabarse en un CD-R cualquiera. Ojo, que aunque el fichero RAR ocupa sólo 57 KBytes, el WAV resultante ocupa algo más de 800 MBytes. Para hacerlo, basta con escoger la opción Crear un CD de audio en cualquier programa de grabación de CDs, como Nero, Brasero… y añadir ese WAV.

Ahora sólo espero que los herederos de John Cage no me demanden por plagio 😀

Personalizando Gnome3

Hace tiempo escribí un artículo criticando el modelo de escritorio de Gnome3 y de Unity, en el que, además, explicaba como configurar un Gnome clásico.

Cuando actualicé a la última Ubuntu (12.04) empecé a tener problemas con los escritorios al usar Gnome Classic, y después de la buena crítica de un amigo, decidí darle otra oportunidad a Gnome3. Pero aún así no olvidaba las carencias que describí en mi anterior artículo, así que decidí personalizarlo a mi gusto.

Lo primero que hice fue activar los iconos en el escritorio. Para ello instalé el programa gnome-tweak-tool, y escogí la opción correspondiente, como se ve en esta imagen:

Lo segundo fue buscar extensiones que supliesen las carencias que veía. Fue entonces cuando me llevé una muy agradable sorpresa con el nuevo sistema para instalar extensiones de Gnome 3.4: en lugar de tener que bajar un fichero, copiarlo en alguna carpeta y rezar para que funcione, los desarrolladores han creado una página web (https://extensions.gnome.org/) que permite simplificar de manera notable este proceso. Dicha página se integra con el navegador y permite instalar extensiones con un simple click en el interruptor que aparece en la parte izquierda. También permite, desde la misma página, desinstalar, actualizar, desactivar y configurar las extensiones instaladas (aunque las dos últimas acciones también se pueden hacer «en local»).

La cantidad de extensiones disponibles es muy grande (más de 170 en el momento de escribir estar líneas), y cubren la práctica totalidad de las necesidades del usuario medio. Las que instalé en mi sistema son las siguientes:

  • Alternative Status Menu: añade al menú de usuario la opción de apagar el equipo
  • Dash and Overview Click Fix: hace que al pulsar sobre el icono de una aplicación que ya está lanzada, se lance otra copia en lugar de mostrar la que ya existe
  • Dash to dock: convierte la barra de iconos de la izquierda en un dock. También permite cambiar de escritorio con la rueda del ratón
  • Frippery Applications Menu: sustituye el botón Actividades por un menú de aplicaciones como el de Gnome 2 en la parte superior izquierda (la tecla Windows sigue entrando en dicho modo)
  • Maximus: elimina la barra superior de las ventanas maximizadas, lo que permite ganar un poco de espacio extra
  • Remove Accesibility: elimina el icono de accesibilidad en la barra superior
  • System Monitor: añade gráficas de uso de la CPU, memoria, etc. (configurable)
  • Windows Alt Tab: hace que las teclas Alt+TAB sólo escojan entre las ventanas del escritorio actual, en lugar de entre las de todos los escritorios
  • Workspace Indicator: muestra un icono con el escritorio actual, y permite cambiar de escritorio desde él
  • Workspace Navigator: permite usar las flechas arriba y abajo para cambiar de escritorio en la vista de Actividades

La única extensión que echo en falta es una que impida que Gnome 3 entre en modo Actividades cuando el escritorio actual se queda sin ventanas.

Lo sorprendente es que, al final, he acabado acostumbrándome a lanzar las aplicaciones mediante la técnica de pulsar la tecla Windows, para entrar en el modo Actividades, y teclear el nombre de la aplicación que quiero. Para las más usadas es mucho más cómodo (por lo que tengo que comerme mis palabras en el anterior artículo). Para las menos, sin embargo, sigue siendo más cómodo el menú de aplicaciones que el sistema nativo de Gnome 3, razón por la que lo conservo.

El maximizar las ventanas simplemente arrastrándolas hasta la parte superior me resulta comodísimo, y el ponerlas a media pantalla arrastrándolas a un lateral es una opción realmente útil cuando uno tiene que comparar textos o copiar de un lado a otro. Además, he acabado acostumbrándome al sistema de escritorios virtuales dinámicos, y de hecho creo que es la mejor opción para aquellos usuarios noveles que nunca los han utilizado.

Así pues, creo que debo comerme parte de mis palabras y reconocer que Gnome 3, aunque con algunos defectos (nada en este universo es perfecto), sí es un escritorio perfectamente válido, y muy sencillo de adaptar a las necesidades de cada uno.

FBZX y WebTV

Después de la vorágine de la tesis por fin tengo tiempo libre, así que acabo de subir dos nuevas versiones, una de FBZX y otra de Transmission para WebTV.

En el primer caso hemos corregido algunas instrucciones que todavía no estaban del todo bien. Pero el gran cambio es, sin duda, la completa reescritura de la emulación del AY-3-8912, el chip de sonido polifónico del Spectrum 128. Mi emulación era bastante inexacta, y aunque funcionaba razonablemente bien con código estándar, no lo hacía bien con programas que utilizasen trucos, tales como reproducir sonido PCM variando el volumen de salida. El nuevo código, realizado íntegramente por Fabio Olimpieri, sigue minuciosamente la documentación oficial del chip, y consigue unos resultados excelentes.

Respecto a la nueva versión de Transmission para el WebTV, los cambios son relativamente pequeños: para empezar, actualicé transmission a la versión 2.61, la más reciente; por otro lado ahora se puede navegar por carpetas para descomprimir ficheros, buscar subtítulos y reparar vídeos AVI. Por último, eliminé la generación de depuración en aMule, a ver si así se solucionan los problemas que han reportado algunos usuarios.