10 de Agosto, 2014

Reproduciendo música en bucle en SFML

En SFML 2.1 tenemos una clase específica para reproducir música de fondo: sf::Music.

Su principal característica es que permite reproducir grandes ficheros haciendo streaming desde disco, con lo que no es necesario tener todo el fichero en memoria. Esto contrasta con sf::Sound, que se utiliza para sonidos en los que no puede haber latencia (se reproducen inmediatamente), y por lo tanto se almacenan en memoria.

Esta distinción está prensente en la mayoría de las librerías que he usado, aunque no siempre se utliza un interfaz diferente (por ejemplo, en pyglet es solo un parámetro en las funciones que cargan los ficheros de audio).

La clase sf::Music es bastante útil, pero le falta una funcionalidad que tampoco está disponible en muchas librerías, pero que en SFML me ha costado bastante implementar :(.

Un requerimiento frecuente cuando se reproduce música de fondo es que el fichero de audio tenga dos partes: la introducción y el cuerpo del bucle que se repite una vez la introducción ha terminado.

Hay distintas formas de implementar esto. A veces es más sencillo partir la música en dos ficheros y reproducir la segunda parte en bucle una vez la primera ha terminado, otras veces es mejor especificar el punto desde el cual el audio debe repetirse.

Para el primer caso normalmente es necesario que la librería nos permita usar un callback cuando un fichero de audio termina de reproducirse (este sistema lo uso en Javascript y Python), el segundo se puede implementar de diferentes formas, pero básicamente se trata de empezar la reproducción en un punto x en lugar de 0.

SFML utiliza un thread para reproducir el audio de forma que nuestro programa nunca se bloquee esperando operaciones con el audio. Esto en general es una buena idea, pero el sf::Music no proporciona opciones para comunicar los dos hilos de ejecución: el que corre nuestra aplicación y el que reproduce el audio. Lo mejor que tenemos en sf::Music es onGetData y onSeek; ambos ejecutados en el hilo que reproduce el audio.

La solución que he implementado en The Legend of Traxtor es la siguiente:

#include <SFML/System/Time.hpp>
#include <SFML/System/Mutex.hpp>
#include <SFML/System/Lock.hpp>
#include <SFML/Audio/Music.hpp>

class ExtMusic : public sf::Music
{
public:
    ExtMusic() : m_ext_loop(false) { }

    void setLoopOffset(float offs) {
        sf::Lock lock(m_ext_mutex);

        m_offs = sf::seconds(offs);
        m_ext_loop = true;
    }

protected:
    bool onGetData(SoundStream::Chunk &data) {
        sf::Lock lock(m_ext_mutex);

        bool result = sf::Music::onGetData(data);
        if (!result && m_ext_loop)
        {
            onSeek(m_offs);
            if (!data.sampleCount)
                sf::Music::onGetData(data);
            return true;
        }

        return result;
    }

private:
    sf::Mutex m_ext_mutex;
    sf::Time m_offs;
    bool m_ext_loop;
};

La idea de la clase ExtMusic (que extiende la clase sf::Music) es indicar desde qué segundo queremos que se repita el audio, y la reprodución saltará a ese punto en el fichero una vez nos quedemos sin datos para reproducir.

Además hay que usar un Mutex para garantizar el acceso exclusivo a los datos porque setLoopOffset se llamará desde un hilo que es distinto al que llamará a onGetData.

Se supone que SFML proporciona sf::SoudSource para que implementemos nuestras propias historias para reproducir audio, pero en mi caso era mucho más sencillo aumentar la funcionalidad de sf::Music que liarme con un interfaz de más bajo nivel como el que proporciona sf::SoundSource.

Al final el resultado es algo hacky (e incompatible con los bucles normales), pero bastante simple. La buena noticia es que alguien ha contribuido una implementación de bucles con punto de inicio, con lo que futuras versiones de SFML ya no tendrán esta limitación ;).

Anotación por Juan J. Martínez, clasificada en: cpp, sfml, programming.

Los comentarios están cerrados: los comentarios se cierran automáticamente una vez pasados 30 días. Si quieres comentar algo acerca de la anotación, puedes hacerlo por e-mail.

Algunas anotaciones relacionadas: