Introducción a la arquitectura de LoLa Sabemos que la función de LoLa es capturar sonido, aplicarle distintas transformaciones y reproducirlo, todo ello tratando de cumplir los requisitos de baja latencia, pasemos entonces a explicar cómo se ha abordado su arquitectura. Buffers de sonido Antes de explicar los módulos diferenciados de LoLa y cómo y cuándo se trasmiten el sonido entre sí, hay que explicar cuál es el “vehículo” que transportará el sonido que ha sido capturado. El sonido dentro de LoLa no se transportará como un simple flujo de bytes, sino que se utilizarán unos objetos contenedores, que llamaremos buffers, que lo encapsularán y que proporcionarán información acerca del sonido que almacenan, como puede ser la frecuencia de muestreo a la que se capturó, el número de bits que ocupa cada muestra y el número de canales que tiene. Estos buffers serán instancias de alguna clase descendiente de CBuffer. LoLa cuenta con una jerarquía de buffers, donde cada clase de la jerarquía se utilizará para encapsular un tipo de sonido capturado con unos parámetros determinados. Por ejemplo tendremos clases diferenciadas para almacenar sonido mono o estéreo, y dentro de estas a su vez habrá clases específicas según el número de bits que tenga cada muestra. Así, al tener el sonido almacenado en buffers, podemos conocer con facilidad la configuración de ese sonido, esto es, la frecuencia a la que ha sido muestreado, el tamaño de cada muestra y el número de canales diferentes. Además mediante el uso de estos valores podemos saber cuántos bytes ocupa a partir de su duración en tiempo, y viceversa, cuál es su duración en tiempo a partir del número de bytes que ocupa. Partes de LoLa La arquitectura de LoLa constará de cuatro partes o módulos fundamentales(sin contar la interfaz, que la consideraremos como algo independiente a LoLa), que son las que colaborarán entre sí para que LoLa funcione y haga aquello para lo que ha sido construido. Dichas partes se enumeran a continuación, junto con la labor que realizan: Módulo capturador: Encargado de recoger el sonido capturado por la tarjeta de sonido y de dárselo a sus clientes en la forma apropiada. Módulo reproductor: Encargado de reproducir el sonido transformado en una ruta de audio. Módulo transformador: Encargado de aplicar distintas transformaciones al sonido. Módulo de control: Encargado de coordinar los tres módulos anteriores entre sí, y de ocultarlos por completo a la interfaz(o a cualquier otro módulo que desee usar LoLa). A continuación se explicarán más detalladamente cada uno de estos módulos: Módulo capturador El módulo capturador será el encargado de recoger el sonido que llega a la tarjeta de sonido. Para ello utiliza las DirectX(en concreto las DirectSound 8.1) como medio de comunicación con la tarjeta. Su labor es muy simple, retornar al cliente del módulo el sonido capturado por la tarjeta de sonido en forma de buffer. Éste módulo consta de dos partes diferenciadas para llevar a cabo sus funciones. Por un lado cuenta con un objeto que es el que se encargará de recoger el sonido de la tarjeta usando las DirectSound 8.1, como ya se dijo antes. Dicho sonido lo devolverá como un flujo de bytes de tamaño variable, sin proporcionar ninguna clase de información sobre el número de canales, ni la frecuencia a la que ha sido muestreado ni el número de bits que ocupa cada muestra. El motivo por el que no devuelve un buffer es para hacer este objeto más portable, y así poder llevarlo a otros sistemas que no reconozcan objetos buffer. Y por otro lado cuenta con otro objeto encargado de recoger el sonido del objeto anterior y encapsularlo dentro de buffers del mismo tamaño. La razón para hacer esto es que las rutas de audio deben trabajar con buffers de tamaño fijo debido a las realimentaciones(ver apartado del módulo de transformación) Módulo reproductor Encargado de reproducir sonido por medio de la tarjeta de sonido. Como el módulo capturador también usa las DirectX 8.1 para llevar a cabo sus funciones. Al igual que el módulo capturador cuenta con dos partes diferenciadas. Una que usa las DirectSound 8.1 para reproducir el sonido que le llega en forma de un flujo de bytes, y otra que recibe los buffers que se desean reproducir. La labor de esta última consistirá en sacar el sonido del buffer y enviárselo al objeto anterior. El motivo de haber organizado el módulo de esta manera responde a las mismas razones que el módulo capturador. Módulo transformador Sin duda el módulo más complejo de los que componen LoLa. Tras la aparente simplicidad que oculta la labor de transformar sonido, se esconde una compleja red de filtros que realizan esta labor. Antes de entrar en el funcionamiento del módulo hay que aclarar lo que es un filtro, sus distintos tipos y cómo funcionan éstos. Una ruta de audio logra la transformación de sonido mediante el trabajo conjunto de diferentes objetos que llamaremos filtros. Cada uno de esos filtros no es más que un objeto capaz de trabajar con el sonido contenido en un buffer, ya sea proporcionando buffers, consumiendo buffers o transformándolos. Dichos filtros estarán relacionados entre sí formando una red arbitrariamente compleja, de forma que cada uno sabe qué filtro o filtros le preceden y/o le siguen. Así cuando queremos transformar un buffer de sonido, este llega a la ruta de audio, la cual no es más que la citada red de filtros, y a partir del filtro inicial va pasando al que le sigue, así hasta llegar al filtro terminal de la ruta de audio. Una vez llegado a este punto el sonido que llega será el que se va a reproducir usando el módulo reproductor. No todos los filtros son iguales, sino que se subdividen en cinco familias, cada una con una función diferente, pero todas ellas con la misma filosofía de ser objetos capaces de trabajar, de una forma u otra, con buffers de sonido. Las cinco familias de filtros son: 1. Efectos: Filtros que sólo tienen una entrada y una salida. Su labor será la de modificar, de una forma u otra, el sonido que les llega en forma de buffer en su entrada y transmitirlo al filtro que tienen a su salida. 2. Filtros unión: Filtros con dos entradas y una salida. Fusionan, de una manera u otra, los dos buffers que le llegan en un único buffer, que transmiten al filtro que tienen a su salida. 3. Filtros disyunción: Filtros con una entrada y dos salidas. Dividen el buffer que les llega a la entrada en dos buffers, transmitiendo cada uno por cada una de sus salidas hacia los filtros que le siguen en la ruta de audio. 4. Filtros iniciales: Filtros sin entrada y con una salida. No pueden estar precedidos por ningún filtro, por lo que no se les puede enviar buffers. Su labor consiste en generar buffers y enviárselos al filtro que les sigue en la ruta de audio. La manera en la que generan sonido es independiente de ellos, ya que ellos lo único que hacen es obtener sonido de algún objeto capaz de producirlo(fichero WAV, objeto generador de ondas senoidales, etc...) y encapsularlo dentro de un buffer. 5. Filtros terminales: Filtros con una entrada y ninguna salida. Son capaces de recibir buffers, pero no de enviarlo. Su labor es la de permitir la utilización del buffer entrante por objetos externos a la ruta de audio, como puede ser un objeto que almacene el sonido en disco a modo de ficheros WAV o un objeto que almacene el buffer temporalmente para ser leído posteriormente por un objeto que lo represente gráficamente. Es importante destacar una característica de los efectos, y es que están especializados en función del número de bits por muestra que tenga el buffer de sonido a transformar, pero no para el número de canales ni para la frecuencia de muestreo. ¿Qué se quiere decir con especializados? Pues sencillamente que la clase CEfecto es una clase base abstracta, y que tiene clases derivadas que son las que saben cómo transformar buffers que almacenen sonido cuyas muestras ocupen uno u otro número de bits. Pero ¿por qué se especializan para el parámetro bits por muestra y no para la frecuencia de muestreo ni para el número de canales? La respuesta es sencilla: porque no hace falta. Cuando transformamos un sonido la transformación se realiza muestra a muestra, no nos importa la cadencia con la que se tomaron dichas muestras, ni si cuenta con uno u ochenta canales. Lo único que nos importa para la transformación de sonido son las muestras. Pero como sabemos las muestras pueden estar almacenadas en un número de bits variable(8 ó 16 en la versión actual de LoLa), y por tanto no será lo mismo transformar muestras que ocupen uno u otro número de bits. Esto se explica claramente con un ejemplo: El efecto volumen lo que hace es aumentar o disminuir la amplitud de una onda, por lo que su implementación consiste simplemente en multiplicar cada muestra por un número. Si tras dicha multiplicación se produce un overflow o un underflow porque el valor resultante no puede ser representado con el número de bits de que disponemos, debemos truncar ese valor al valor límite permitido con ese número de bits, ya que si no se hiciese así podría ocurrir, por ejemplo, que una señal que se amplifique mucho pase a tener valores negativos tras la aplicación del efecto, y por tanto estaría funcionando mal. Pero la labor del módulo transformador no se limita al mantenimiento de la red de filtros, sino que también debe crearla. Las redes se crearán a partir de ficheros externos que contendrán la estructura de la misma, indicando los diferentes tipos de filtros que deseamos. Dichos ficheros se llamaran ficheros LML y no se explicarán aquí, ya que hay una sección dedicada a ellos. Pero algo habrá que decir de la creación de la red. Estos ficheros están escritos en un lenguaje XML, y por tanto se necesitará un parser para leerlos. En LoLa se usa el API de Xerces para leer estos ficheros, en lugar de hacerlo LoLa directamente. Así, utilizando ese API crearemos una estructura de datos en memoria que lo represente, y esa estructura de datos será lo que utilice el módulo transformador para crear la red de filtros. Nótese que así se hace independiente éste módulo de los ficheros LML y del API que se utilice para leerlos. Módulo controlador Éste módulo, a pesar de ser el más simple de todos, realiza las labores de control de los tres anteriores, además de la ocultación de los mismos con respecto a las clases clientes de LoLa(como por ejemplo la interfaz). Su función se limita a recoger sonido del módulo capturador, enviárselo al módulo transformador, recoger el buffer de sonido transformado y enviárselo finalmente al módulo reproductor. Esta secuencia de acciones se realizará constantemente mientras LoLa esté funcionando, por lo que este modulo creará un hilo cuya única función será hacer esto ininterrumpidamente, mientras no se desee parar. También será mediante el módulo controlador como la interfaz modificará los parámetros de los efectos constituyentes de la ruta de audio, porque recordemos que la interfaz está aislada de los tres módulos anteriores por medio de éste.