FluidSynth superado. Ahora empieza lo bueno.

Por fin puedo decir que he echado un pulso a fluidsynth, sus python bindings (pyFluidSynth, see original project and my personal fork) y a todas las dificultades que un proyecto de estas características te pone delante. Pero lo primero es lo primero, así que aquí tenéis una Demo de como funciona fluidsynth en una Raspberry Pi 2 B+ (Raspbian), haciendo llamadas mediante los python bindings de fluidsynth:

Y ahora vamos con los detalles. El video muestra simplemente una llamada a liveDemo.py, un script que he preparado para probar 20 segundos del programa 0 y 20 segundos del programa 50 de cualquier soundfont que tengamos (por defecto carga /usr/share/sounds/sf2/FluidR3_GM). Así que los pasos para probarlo, desde una Raspberry Pi conectada a internet con Raspbian son los siguientes.

En primer lugar, nos aseguramos de tener instalado FluidR3_GM y las librerías de python de ALSA:

sudo apt-get install fluid-soundfont-gm python-pyalsa

Posteriormente, hacemos un clone de pyFluidSynth, con el código:

git clone https://github.com/pakitochus/pyfluidsynth.git

Navegamos hasta el lugar donde está la instalación y ejecutamos:

cd pyfluidsynth
sudo python setup.py install

Y en principio, ya podríamos ejecutar el test con:

cd test
python liveDemo.py

Por supuesto, hace falta tener un teclado midi conectado a la raspberry, yo sugiero MIDI USB. En el script, se presupone que el puerto en el que está conectado es el 20,0, pero esto no tiene por qué ser así. La forma correcta de saber cual es el dispositivo que tenemos completado es mediante el comando:

aconnect -i

que listará algo así como:

cliente 0: 'System' [tipo=kernel]
    0 'Timer           '
    1 'Announce        '
cliente 14: 'Midi Through' [tipo=kernel]
    0 'Midi Through Port-0'
cliente 23: 'MIDI KEYBOARD' [tipo=kernel]
    0 'MIDI KEYBOARD MIDI 1'

Suponiendo esos datos, para cambiar el que está por defecto, editamos el archivo liveDemo.py, y en la línea 29, donde aparece

sender = (20, 0)  # Modify according to the current port of the USB MIDI input

cambiamos por

sender = (29, 0)

De igual modo, para cambiar la soundfont a utilizar o modificar la ruta, vamos a la línea 22 del archivo, y sustituimos la línea

sfid = fs.sfload("/usr/share/sounds/sf2/FluidR3_GM.sf2")

por la ruta hasta el archivo de soundfont que queramos. En el video, he utilizado una colección que he recopilado y creado -a partes iguales- llamada ChusoCol, que podéis encontrar en Sourceforge (pronto subiré la ChusoCol 2, la del video).

Y eso es todo. No deja de ser una demostración de como funciona el fork de pyFluidSynth. Ahora es cuando viene lo bueno: convertir la RPi2 en un single-purpose computer y añadir todos los controles para usar el PiFace Control and Display module.

Esto no ha hecho más que empezar. Pero ya hay un paso menos que dar.

FluidSynth superado. Ahora empieza lo bueno.

Tocando mis primeras notas

Por primera vez, he logrado lanzar fluidsynth desde una terminal python, crear su correspondiente driver MIDI y tocar algunas notas con el teclado en directo. A partir de aquí, es todo mejorar.

El problema que estaba teniendo era con la función new_fluid_midi_driver(settings, handler, event_handler_data), en el que en la documentación aparece como que hay que llamarlo (en C) de esta forma:

fluid_settings_t* settings;
fluid_midi_driver_t* mdriver;
settings = new_fluid_settings();
mdriver = new_fluid_midi_driver(settings, handle_midi_event, NULL);

sugiriendo el uso de fluid_midi_router_handle_midi_event() como handler callback. Finalmente, la mejor opción para mi fue:

mdriver = new_fluid_midi_driver(settings, fluid_synth_handle_midi_event, synth)

O sea, que había que la función fluid_synth_handle_midi_event es la pancea y en ningún sitio de la documentación de API te la especifican. Bien por fluidsynth. Y usar el propio objeto sintetizador synth como event_handler_data.

Tocando mis primeras notas

A estas alturas

Y cuando ya pensaba que tenía el fork de pyFluidSynth casi terminado, me doy cuenta de que todavía me falta lo más importante: añadir el driver midi. Sin embargo, conforme uno va implementando cosas se da cuenta de que cada vez es más complicado añadir funcionalidad desde python a una API en C.

En esta ocasión, utilizando las funciones de la API, logro crear un driver MIDI al que conectar luego un teclado. Pero en cuando mando la primera nota, me manda un bonito SYSSEG: reading NULL VMA que hará las delicias de todos. ¿Por qué tengo un puntero nulo? Creía que ctypes se encargaría de todo.

Este es el código que manejo ahora mismo:

    def start_midi(self, mididriver='alsa_seq'):
        """
        Starts the MIDI driver to allow the MIDI keyboard interaction.
        :param mididriver: name of the midi driver, that can be one of these:
            'alsa_raw', 'alsa_seq', 'coremidi', 'jack',
            'midishare', 'oss', 'winmidi'
        :return:
        """
        if mididriver is not None:
            assert (mididriver in ['alsa_raw', 'alsa_seq', 'coremidi', 'jack',
                                   'midishare', 'oss', 'winmidi'])
            fluid_settings_setstr(self.settings, 'midi.driver', mididriver)
            # Optionally: sets the real time priority to 99.
            fluid_settings_setnum(self.settings, 'midi.realtimeprio', 99)
        self.midi_driver = new_fluid_midi_driver(self.settings, fluid_midi_router_handle_midi_event, new_fluid_midi_event)
        return self.midi_driver

No tengo ni idea de como voy a salir del paso, porque en principio parece que la función fluid_midi_router_handle_midi_event debería pasar todos los comandos de entrada al sintetizador, pero en lugar de ello, da fallo de segmentación. No sé si es por el midi.realtimeprio (que siempre da un warning), o por la función en sí, o por el recientemente añadido new_fluid_midi_event, pero la verdad es que estoy bastante estancado. A ver si esta tarde salimos del paso.

A estas alturas

Entre APIs y Ces…

Sí, la interfaz pyFluidSynth que estoy forkeando tiene una función principal: interactuar con la API de fluidsynth, que está escrita en C. Eso lleva a algunos problemas, por ejemplo, en el tipo de datos y estructuras que maneja.

Más o menos podemos decir que la cosa va viento en popa, y he ampliado el número de funciones que la clase Synth posee con muchas de las funciones que provee la API, pero hay cosas con las que he tenido que recurrir a magia negra. Para obtener una lista de todos los instrumentos que posee una función he tenido que crear una interfaz para capturar durante unos momentos la salida del shell. En fluidsynth, hay dos opciones de obtener la lista: construir una serie de estructuras horrendas (con las que interactuar en python se puede convertir en un infierno), o capturar la salida del shell con los comandos inst SF_ID. Así que opté por esta última.

Tras darle muchísimas vueltas, por fin conseguí hacer una clase, que llamé StdoutHandler, y que llamando objeto.freopen() abres un archivo y escribe la salida de la shell en el mismo, y con objeto.freclose() se devuelve la salida al stdout del sistema.

Así que esto es lo que he hecho, por si a alguien le hace falta (basado en este post de StackOverflow):

class StdoutHandler(object):
    def __init__(self, f):
        """Create new stdouthandler, for management of stdin and
        stdout (some methods of Synth DO need to capture stdout stream).
        """
        self.prevOutFd = os.dup(1)
        self.prevInFd = os.dup(0)
        self.prevErrFd = os.dup(2)
        self.newf = open(f, 'w')
        self.newfd = self.newf.fileno()  # The new file output


    def freopen(self):
        """
        Redirects the standard input, output and error stream
        to the established newfd.
        :return:
        """
        os.dup2(self.newfd, 0)
        os.dup2(self.newfd, 1)
        os.dup2(self.newfd, 2)

    def freclose(self):
        """
        Closes the modified input, output and error stream
        :return:
        """
        self.newf.close()
        os.dup2(self.prevOutFd, 1)
        os.close(self.prevOutFd)
        os.dup2(self.prevInFd, 0)
        os.close(self.prevInFd)
        os.dup2(self.prevErrFd,2)
        os.close(self.prevErrFd)
Entre APIs y Ces…