• No se han encontrado resultados

Grado en Ingeniería Multimedia

N/A
N/A
Protected

Academic year: 2022

Share "Grado en Ingeniería Multimedia"

Copied!
74
0
0

Texto completo

(1)

Grado en Ingeniería Multimedia

Trabajo Fin de Grado

Autor:

Sergio Campos García Tutor/es:

Pedro J. Ponce de León Amador

septiembre 2021

(2)

1

(3)

2

Guía de estilo seguida para el desarrollo de la memoria

Para facilitar el desarrollo de la memoria, se ha hecho uso de la plantilla ofrecida por José Vicente Berná Martínez para los alumnos de Ingeniería Multimedia para la redacción de los trabajos de fin de grado.

Versión del documento 7 septiembre 2021

Licencia

Se permite la reproducción, distribución y comunicación pública de la obra, incluso con fines comerciales siempre y cuando reconozca y cite la obra de la forma especificada por el autor o el licenciante.

(4)

3

Resumen

A día de hoy, en el panorama del desarrollo de videojuegos existe un problema. Las herramientas pensadas para la implementación de sonido en el código presentan una serie de dificultades para el programador: O bien la herramienta es relativamente sencilla, pero requiere de una aplicación de alto nivel que exige conocimientos de diseño sonoro para utilizarse, o bien consiste en una aplicación que trate el audio a un nivel más bajo que exija conocimientos de teoría de señales y computación de audio. Sumando a todo esto que el motor que diseñe el programador tendrá que gestionar los recursos y la memoria por su cuenta, el programador se encuentra con varias trabas que aumenta la curva de aprendizaje y tiempo de desarrollo al que se enfrenta.

Este trabajo consiste en el estudio de la problemática de los motores de sonido en el estado actual del desarrollo de videojuegos y el posterior desarrollo de un motor de sonido propio que sea ligero y funcional, que pueda servir para la mayoría de casos tanto de forma práctica para proyectos multimedia, como de forma didáctica en aplicaciones interactivas aplicables en el ámbito de la enseñanza, encargándose totalmente del manejo del audio, su posicionamiento en un espacio acústico 3d y su manejo en memoria, proporcionando al programador una herramienta fácil de usar, con funcionalidades básicas y que gestiona sus propios recursos quitando preocupaciones al programador.

(5)

4

Motivación y justificación general

A lo largo de mi desarrollo en la carrera de Ingeniería Multimedia, he ido encontrando diferentes disciplinas que me han ido gustando. Desde modelado, programación pura o tratamiento de audio entre otras. Con la llegada del ABP, la metodología de trabajo de 4º de carrera enfocado al desarrollo grupal de un proyecto, cada uno nos dedicamos a diferentes materias, a veces más individualmente y a veces más grupalmente. Fue ahí cuando tuve el primer contacto con la programación de audio. Para aquel proyecto utilizamos herramientas sofisticadas de diseño sonoro con la capacidad de crear eventos de sonido que podrían ser posteriormente manejados mediante una API directamente desde el código de la aplicación. La creación de un motor de sonido con esta herramienta me resultó de lo más interesante y enriquecedor, ya que juntaba dos de las disciplinas que más me gustaron en mi estudio: programación y tratamiento de sonido.

Tras el desarrollo de esto, obtuvimos un motor de sonido capaz de reproducir diferentes eventos sonoros. Lo que más me gustó de esto era el cómo fusionaba el diseño sonoro de diferentes eventos sonoros, teniendo montones de funcionalidades, con el manejo en código de estos eventos, administrando su uso y memoria de la mejor forma posible

Siendo un mundo con mucha posibilidad de exploración, considero muy interesante la creación de un motor de sonido totalmente propio, enfocado a manejar audio a bajo nivel, para adecuarse a necesidades más concretas y ofreciendo así una solución más sencilla que otros motores.

Creo que con este proyecto, no solo se tiene la intención de crear un proyecto que sea capaz de servir de herramienta a otros, si no además me funciona como consolidación de los conocimientos que he ido adquiriendo a lo largo de la carrera, enfocado al manejo del código y del audio, más concretamente.

(6)

5

Agradecimientos

Para empezar, dar las gracias a toda la gente, tanto amigos como familia, que ha pasado por mi vida desde el momento de entrar a la carrera, tanto a aquellos que se quedaron como aquellos que se fueron. Gracias a mi grupo de amigos de siempre por darle vida a cada día, a los nuevos de la universidad, tanto a la gente de “Zenotafio Games” por todas las aventuras, juegos hechos y gachamigas por hacer, como a la gente de “Malaka” por haber superado juntos uno de los años más duros de la carrera de la forma tan buena que se hizo. También a los compañeros de mi primer trabajo, a mi tutor por enseñarme como se pueden mezclar los mundos de programación y sonido, y por último pero no menos importante, a los “Malatestaos” por absolutamente todos y cada uno de los momentos desde el comienzo de la universidad.

(7)

6

Citas

Pies calientes y cabeza fría

- Mi abuelo

it's always time to question what has become standard and established

- David Bowie

(8)

7

Índice de contenidos

Resumen ... 3

Motivación y justificación general ... 4

Agradecimientos ... 5

Citas ... 6

Índice de contenidos ... 7

Índice de figuras ... 10

Índice de tablas ... 11

1. Introducción ... 12

2. Planificación ... 14

3. Estudio de viabilidad ... 15

3.1. Análisis DAFO ... 15

3.2. Análisis de riesgos ... 17

4. Estado del arte ... 20

4.1. Orígenes en el desarrollo de videojuegos ... 20

4.1.1. Sonido en los primeros años ... 20

4.2. Metodología actual de desarrollo de videojuegos en grandes empresas ... 22

4.2.1. Herramientas de Alto Nivel para Sonido ... 24

4.3. Desarrollo independiente de videojuegos ... 25

4.3.1. Herramientas de Bajo Nivel en Sonido ... 26

4.4. Problemática en el audio ... 27

4.4.1. Problemas en las herramientas de Alto nivel... 27

4.4.2. Problemas en las herramientas de Bajo Nivel ... 27

4.5. Propuesta ... 28

5. Objetivos ... 29

6. Metodología ... 30

7. Análisis y especificación ... 32

(9)

8

7.1. Requerimientos funcionales base ... 32

7.2. Requerimientos funcionales de tipo componentes ... 33

7.3. Requerimientos No Funcionales ... 34

8. Diseño ... 35

8.1. Planteamiento inicial ... 35

8.2. Herramientas que se usarán ... 38

8.2.1. PortAudio ... 39

8.2.2. Libsndfile ... 40

8.3. Desarrollo y diseño de los métodos ... 40

8.3.1. Inicialización y procesamiento básico ... 41

8.3.1.1. Funcionamiento de un Callback ... 42

8.3.2. Reproducción simultánea de varios audios ... 45

8.3.3. Atribución de volúmenes, panorama y bucles ... 47

8.3.4. Posicionamiento espacial de las fuentes de sonido ... 49

8.3.4.1. Algoritmo VBAP ... 50

8.3.4.2. Implementación en motor ... 51

8.3.4.3. Posibles mejoras al algoritmo y consideraciones ... 56

8.3.5. Optimización en carga y descarga de ficheros ... 58

9. Implementación ... 60

9.1. Sprint 0: Análisis de tecnologías y primeros pasos ... 60

9.2. Sprint 1: Manejo de recursos, reproducción simultánea, volumen y panorama... 61

9.3. Sprint 2: Refactorización de streams y audio estéreo ... 62

9.4. Sprint 3: VBAP y posiciones ... 62

9.5. Sprint 4: Optimizaciones en uso de memoria, reorganización y bucles ... 63

10. Pruebas y validación ... 65

11. Resultados, conclusiones y trabajo futuro ... 68

11.1. Mejoras potenciales ... 69

11.2. Aportación personal ... 69

(10)

9

Referencias y Bibliografía ... 71 Apéndice I ... 73

(11)

10

Índice de figuras

Figura 1. Análisis DAFO ... 15

Figura 2. Chip Ricoh 2A03 ... 21

Figura 3. Chip SN76489 ... 21

Figura 4. Interfaz FMOD Studio ... 24

Figura 5. Interfaz WWise ... 24

Figura 6. Estado de tareas en Trello ... 31

Figura 7. Esquema de funcionamiento de ALSA... 37

Figura 8. Esquema de elementos involucrados en el motor ... 38

Figura 9. Esquema de funcionamiento de PortAudio ... 39

Figura 10. Redistribución de muestras en nuevo buffer ... 43

Figura 11. Esquema básico del motor de sonido ... 44

Figura 12. Visualización del panorama, 30º ... 52

Figura 13. Visualización del panorama, 90º ... 52

Figura 14. Visualización del panorama, 180º ... 52

Figura 15. Representación de fuente audible entre par de altavoces ... 54

Figura 16. Representación de fuente no audible entre par de altavoces ... 54

Figura 17. Representación de fuente virtual sin considerar eje Z en sistema de dos altavoces . 56 Figura 18. Representación de posible solución para el eje Z en sistema de dos altavoces ... 57

Figura 19. Sonic Visualiser ... 65

Figura 20. Captura de prueba 1 ... 66

Figura 21. Captura de prueba 2 ... 67

Figura 22. Fórmula para el ángulo... 73

Figura 23. Fórmula para la extensión ... 73

(12)

11

Índice de tablas

Tabla 1. Planificación temporal TFG ... 14 Tabla 2. Análisis de Riesgos-Probabilidad-Gravedad ... 17 Tabla 3. Análisis de Riesgos-Prevenciones/Soluciones ... 18

(13)

12

1. Introducción

Actualmente en la industria del videojuego, existen una gran gama de ofertas de motores que nos ayudan a cumplir nuestros objetivos de desarrollo del videojuego, tanto en el contexto gráfico, como en el de físicas o audio.

Con por ejemplo los motores gráficos, podemos encontrar tanto opciones de alto nivel junto con motores más complejos con físicas incluidas como el de Unity [1], o alternativas más de bajo nivel como Irrlicht [2], que ofrecen soluciones menos costosas a cambio de una mayor exploración del código.

En el caso de los motores de audio, la gestión del sonido en aplicaciones no lineales (p. ej., videojuegos) exige el desarrollo de herramientas específicas de alto nivel que permitan abstraer las API de bajo nivel de los sistemas operativos, facilitando la manipulación en vivo de los eventos sonoros desde el código de la aplicación.

Existen en el mercado ya algunas herramientas profesionales (FMOD [3], WWise [4], etc.) que cubren estas necesidades. Por otro lado, existen herramientas de diseño sonoro que permiten la síntesis y manipulación de sonido de forma programática, lo cual tiene un gran potencial de uso en aplicaciones no lineales.

El problema que existe con estas herramientas reside principalmente en dos cuestiones:

Primeramente, muchos de estos motores se componen de una aplicación en la que se crearán diferentes eventos sonoros que se reproducirán en el proyecto, más luego una API que será la encargada de usar esos eventos y manipularlos dentro del código. Es cierto que esta metodología ofrece gran versatilidad y puede conseguir resultados muy buenos, pero añade una capa de complejidad que en muchos tipos de proyectos resulta innecesaria.

Por otro lado, no existen aplicaciones orientadas a sonido que traigan una infraestructura hecha y que gestionen sus elementos, como ocurre con Irrlicht en gráficos. En el sonido, el usuario se tendrá que involucrar directamente en procesos de computación de sonido que puede que no sepa tratar como es debido, además de que tendrá que desarrollar por él mismo toda la arquitectura y gestión de su motor y sus elementos.

(14)

13

Por ello, proponemos el diseño y estudio de un motor de sonido en C + + enfocado a aplicaciones no lineales, con el objetivo de ofrecer soluciones a los principales problemas de la implementación de sonido de un videojuego, como son la creación de eventos de sonido en tiempo real con parámetros variables como volumen, bucles y panorama, su posicionamiento en el espacio con posibilidad de varios altavoces en un mismo plano y el correcto manejo de la memoria desde el propio motor. Cumpliendo estos objetivos, conseguimos por un lado reducir al mínimo las preocupaciones del programador en lo relativo al audio, además de ofrecer un mejor rendimiento y consumo total de la aplicación gracias a que el motor trabaja directamente a bajo nivel, abstrayendo al usuario de tareas como la gestión de memoria.

(15)

14

2. Planificación

Para que podamos organizarnos convenientemente, vamos a realizar una planificación de los tiempos que dedicaremos en principio a las tareas, de forma que tengamos más claro el tiempo aproximado dedicado a cada tarea.

Es importante para la planificación, considerar que a lo largo del periodo de trabajo dedicado al TFG, se está compaginando a su vez con un trabajo de media jornada que incluye fines de semana, cuya duración finalizará en mayo, donde se comenzará con las prácticas de empresa a media jornada también, pero dejando fines de semana libres. Por ello, se tiene planeado comenzar el proyecto de código en sí el 15 de marzo, y trabajar en él durante 10 semanas a 20 horas semanales, con fecha límite, el 1 de junio para comenzar a hacer la memoria del proyecto.

Para la memoria del proyecto, se buscará dedicar a la memoria 3 meses de trabajo a 20 horas semanales también, teniendo en mente la entrega para la convocatoria de septiembre de 2021.

En la siguiente tabla, podemos ver lo que es un planteamiento inicial del desarrollo ideal del proyecto.

Tabla 1. Planificación temporal TFG

Materia Tiempo total Fecha límite fin

Creación del motor de sonido 10 semanas 1 junio

Introducción Planificación Estudio Viabilidad Objetivos

Metodología

4 semanas 31 junio

Estado del Arte

Análisis y Especificación Diseño (Primera mitad)

4 semanas 31 julio

Diseño (Segunda mitad) Implementación

Pruebas y validación

Resultados, conclusiones y trabajo futuro

4 semanas 31 agosto

(16)

15

3. Estudio de viabilidad

En este apartado vamos a analizar diversos factores del proyecto al respecto de su viabilidad.

Antes de comenzar con una trabajo que nos va a llevar especial tiempo, debemos de fijarnos en la situación en la que se encuentra el entorno de los motores de audio, y ver cómo puede jugarnos en contra o a favor. Para esto, es muy conveniente la realización de un análisis DAFO, que veremos más adelante y nos permitirá encauzar mejor el proyecto. Por otro lado, también tenemos que considerar los eventos que pueden ocurrir durante el desarrollo, que supongan un riesgo para la finalización del mismo, análisis que veremos también más adelante.

3.1. Análisis DAFO

Un análisis DAFO (Debilidades - Amenazas - Fortalezas - Oportunidades), consiste en un análisis de los factores internos y externos a los que se enfrenta un proyecto. De esta forma, centraremos mucho más la dirección que tomará un proyecto, sabiendo cuales son los puntos fuertes en los que más nos tenemos que fundamentar, y los puntos flacos que más tendremos que vigilar e intentar pulir para que hagan peligrar el proyecto de la menor forma posible.

Vamos a hacer el análisis a raíz de los elementos de la figura 1.

Figura 1. Análisis DAFO (Fuente propia)

(17)

16

Centrémonos primeramente en las fortalezas, las propiedades internas del trabajo que jugarán a nuestro favor y que lo hacen especial. Las principales características que tenemos aquí son que el motor será ligero y sencillo, usable en proyectos no excesivamente complejos. Con este proyecto, se ofrece un motor de sonido acoplable al código, de baja dificultad tanto de entendimiento como de manejo, y que provee de suficientes características básicas en lo relativo al sonido para lograr una experiencia sonora correcta, sin ofrecer más funcionalidades ni dependencias de las que se van a necesitar, y sin dejar atrás la eficiencia de las mismas.

Por otro lado, desde el punto de vista interno, debemos de contemplar también las debilidades de nuestro proyecto. Son factores como que este proyecto es el primero que se desarrolla de este tipo y siempre es susceptible a fallos y mejoras potenciales que no se deben de pasar por alto. Otra debilidad es el hecho de que el tiempo de desarrollo se ha limitado al tiempo disponible para un Trabajo de Fin de Grado, por lo que se ha dedicado más tiempo a la implementación de funcionalidades, que a la optimización de los recursos y los tratamientos del sonido.

Ya pasando al punto de vista externo, analicemos las oportunidades que tenemos con este proyecto, enfocado al entorno en el que estamos. Como ya comentamos, el mundo de los motores de sonido es un campo con mucha posible exploración y que actualmente se ha reducido a dos formas que ya vimos: O bien APIs solventes pero dependientes de otras aplicaciones, o librerías y herramientas de código propio de manejo de audio, pero más complejas de entender y manejar. El motor que se desarrolla, tiene como puntos fuertes con respecto a los demás que no existe un concepto que mezcle lo mejor de los dos acercamientos actuales, ya que al estar hecho desde el punto de vista del programador, elimina la dificultad de manejar una aplicación de escritorio externa, además de realizar internamente todos los cómputos de audio a bajo nivel para que el programador solo tenga que conectar el motor y realizar las llamadas necesarias para tener todo en funcionamiento.

Externamente, también tenemos que ver las posibles amenazas a las que se somete el trabajo, como que las funcionalidades no tan numerosas pueden hacer más atractivas otras alternativas, sobre todo si el usuario ya conoce sobre diseño sonoro o teoría propia. Por otro lado, hay que estar al tanto también de todos los posibles avances que se hacen en el campo, ya que muchos usuarios pueden desarrollar sus propios motores con la misma intención que nosotros teniendo la posibilidad de ser mejor optimizados o más funcionales.

(18)

17

3.2. Análisis de riesgos

A la hora de trabajar en un proyecto de este estilo, dado que requiere de tiempo y dedicación, existen muchos factores que pueden ralentizar este proceso. A continuación, se detallan una serie de factores que pueden influir negativamente en el desarrollo del proyecto, junto a posibles acciones que se van a tomar.

Cada uno tendrá asociada una probabilidad. Que un problema sea de probabilidad baja significa que es improbable que surja a lo largo del desarrollo del proyecto, de la misma forma que una alta probabilidad indica que seguramente nos encontremos con esta situación en algún momento. La gravedad, que también variará en estos términos, si es baja indicará que no supondrá demasiado impedimento para el desarrollo del proyecto, incluso si no se toman medidas para solventarlo. Igualmente, una gravedad alta nos indica que se deben tomar medidas si no queremos que el desarrollo se ralentice hasta el punto que pueda peligrar el resultado final del trabajo.

Tabla 2. Análisis de Riesgos-Probabilidad-Gravedad

Problema Probabilidad Gravedad

Desconocimiento de las tecnologías, librerías y herramientas que se usarán en el proyecto

Alta Baja/Media

Enfermedad leve Baja Baja

Enfermedad más grave/Indisposición Muy baja Media/Alta

Falta de herramientas de trabajo (ordenadores) Baja Baja Caída de internet o imposibilidad de conexión de red Muy baja Baja Pérdida/Destrucción de herramientas de trabajo que

albergan el contenido del proyecto

Baja Alta

Aparición de compromisos ajenos al proyecto y a la universidad

Alta Baja

Aparición de compromisos propios de la universidad, como otras asignaturas

Alta Media

(19)

18

Tabla 3. Análisis de Riesgos-Prevenciones/Soluciones

Problema Prevención/Solución

Desconocimiento de las tecnologías, librerías y herramientas que se usarán en el proyecto

Dedicar tiempo a leer documentación oficial, ver tutoriales, explorar proyectos que utilicen las herramientas que vamos a usar

Enfermedad leve Dado que es una enfermedad leve, se

puede continuar trabajando de forma normal. En caso de necesitar algún tipo de descanso o tratamiento, tomarlo para encontrarse operativo lo antes posible

Enfermedad más grave/Indisposición Si la enfermedad nos deja indispuestos durante mucho tiempo, contamos con que la planificación del proyecto contempla estos imprevistos. De no ser el caso, una vez se esté en condiciones, dedicarle mayor cantidad de tiempo que el inicialmente previsto

Falta de herramientas de trabajo (ordenadores) Tenemos que asegurarnos de tener en todo momento una estación de trabajo sustituta, ya sea en forma de segundo ordenador, máquina virtual, u ordenador de sitio público, como la universidad

Caída de internet o imposibilidad de conexión de red Por suerte para el desarrollo del proyecto, la única necesidad de conexión a internet es la de buscar información sobre el mismo, ya que no necesita conexión de red para funcionar. Se puede prevenir descargando los diferentes contenidos para posteriormente usarlos (Páginas de tutoriales, vídeos, audios, etc) Pérdida/Destrucción de herramientas de trabajo que

albergan el contenido del proyecto

Siempre es conveniente para la seguridad del proyecto conservar diferentes versiones del mismo en diferentes formas. En nuestro caso, conservarlo en forma de repositorio remoto, localmente en la máquina, y en algún pendrive de memoria.

Aparición de compromisos ajenos al proyecto y a la universidad

Una correcta planificación del proyecto debería ser suficiente para que este tipo de compromisos no nos afecten de forma seria al proyecto.

Aparición de compromisos propios de la universidad, como otras asignaturas

De la misma forma, tener una correcta planificación del tiempo de dedicación de cada asignatura parte, para así evitar dedicarle más tiempo del necesario a algo que no lo requiere.

(20)

19

(21)

20

4. Estado del arte

4.1. Orígenes en el desarrollo de videojuegos

El mundo del desarrollo del videojuego ha sido muy cambiante desde su nacimiento. En sus orígenes, la creación era un reto que dedicaba de verdaderos conocimientos de ingeniería, donde los programadores debían de trabajar con muy pocas herramientas y con código perfectamente diseñados para que en la menor memoria posible pudieran incluir niveles, gráficos, sonidos, y todo tipo de algoritmos, mecánicas y funcionalidades que hicieran funcionar el proyecto.

4.1.1. Sonido en los primeros años

En lo relativo al sonido, si nos remontamos a los primeros ordenadores de uso personal, como el IBM PC [5] o el Apple II [6], estos computadores tenían el sonido integrado directamente en la CPU, por lo que crear piezas musicales o de sonido y que no abarcaran total o gran parte del espacio de cómputo era especialmente complicado [7]. Con el avance del tiempo, para obtener mejores resultados y rebajar la carga de trabajo de los procesadores de las máquinas, se comenzaron a implantar los chips de sonido de la conocida época de los 8-bits1, donde al separar el sonido, se podían obtener resultados mucho mejores.

Estos chips tenían cada uno una serie de características únicas, donde la principal diferencia eran las voces de cada chip, lo que sería una primera muestra de lo que sería el concepto de canal a día de hoy. Las voces determinaban cuántos sonidos podían estar sonando a la vez, teniendo cada una posibles formas de onda que darían cierta libertad a los programadores de audio. Esto en ocasiones podía causar problemas como que si ocurría un evento que producía algún sonido, como un golpe, y estaban todas las voces ocupadas por alguna pieza de música que suena en ese momento, alguno de los elementos musicales dejaría de sonar por un momento para hacer hueco en el canal que debe sonar [8]. A pesar de estos defectos propios de las limitaciones de los chips, la mejora era muy considerable frente a la anterior forma de abarcar el audio desde los propios procesadores.

1 Momento en la historia de los videojuegos durante los años 80, donde los procesadores de las consolas se basaban en arquitecturas de 8 bits.

(22)

21

Como ejemplo, tenemos el chip Ricoh 2A03 [9] que se puede apreciar en la figura 2, utilizado en consolas del momento como la NES (Famicom) de Nintendo. Este chip se caracterizaba por tener cinco voces diferentes, dos para ondas cuadradas, una para ondas triangulares, otra para ruidos blancos modificable a ruidos tonales que se utilizaba en ocasiones para percusión, y una voz para reproducción de muestras que se usaba con menos frecuencia.

Figura 2. Chip Ricoh 2A03

(Fuente:https://memim.com/ricoh-2a03.html )

Otro ejemplo puede ser el chip SN76489 [10] que vemos en la figura 3, de Texas Instruments, utilizado en la Sega Genesis o Sega Mega Drive. Este chip, a diferencia del 2A03, tenía cuatro voces, tres de ondas cuadradas y una para ruidos blancos.

Figura 3. Chip SN76489

(Fuente: https://hackaday.com/2020/06/05/how-did-they-get-sampled-sounds-from-an-sn76489-8-bit- sound-chip/)

(23)

22

4.2. Metodología actual de desarrollo de videojuegos en grandes empresas

Con el paso del tiempo, el asentamiento del videojuego en el mundo actual como fuente de entretenimiento globalizado, ha llevado a las empresas a buscar para sus equipos de desarrollo no solo ingenieros capaces de crear código potente capaz de aprovechar al máximo las herramientas que se tienen a día de hoy, siendo capaces de implementar en espacios de memoria relativamente pequeños, inmensas cantidades de información; si no que existen una diversidad de profesionales mucho más especializados en las ramas que van a tratar. Ahora tenemos equipos para el diseño de gameplay, dedicados a crear la narrativa del juego, artistas que dan la estética visual a todo el concepto, diseñadores de niveles que se centran en crear los escenarios en los que el juego se desarrollará, y una muy amplia variedad de programadores de todo tipo enfocados a diferentes disciplinas:

Aquí nos encontramos con los programadores de gameplay, mecánicas y físicas, que desarrollan todo lo relacionado con el funcionamiento del mundo interno del videojuego y como avanza y progresa. Programadores de IA encargados de la creación de algoritmos y procesos de pensamiento para las entidades del juego y hacer que actúen de la manera más realista posible.

Programadores de gráficos, encargados de manejar correctamente los objetos con los que tratan, desde modelados, entornos gráficos y texturas, considerando como punto especialmente delicado el manejo de la memoria, dado el gran tamaño que ocupan estas implementaciones, y programadores del HUD, que se encargan de crear toda la interfaz visual tanto “ingame” como de menús, obteniendo información del entorno y dándosela al usuario además de realizar las acciones que se seleccionan, como poner en pausa o cambiar configuraciones y valores del juego entero.

En lo relativo al sonido, en grandes proyectos existe normalmente una división en el trabajo: Lo más normal es que nos encontremos con tres divisiones: Diseñador sonoro, Compositor Musical y Programador de audio.

(24)

23

El diseñador sonoro de un proyecto es el encargado del planteamiento del sonido de un juego, la estética que va a tener el sonido, buscando transmitir al oyente determinadas sensaciones y emociones. Se encarga también de la consecuente grabación de los efectos de sonido, los Foleys2, etc. Posteriormente, se encargan de la mezcla y la edición de los sonidos para adecuarlos de la mejor manera al proyecto en el que se implementará

Por otro lado, suele involucrarse un compositor, que se encarga de producir las piezas musicales que sonarán en el videojuego.

Por último, tenemos el programador de sonido, encargado de la implementación directa del sonido en el código del juego, procurando que todos los sonidos involucrados se reproduzcan de forma correcta en los momentos debidos y tratando de utilizar la mínima carga de proceso posible. También se encargará de realizar todos los post-procesamientos del sonido que ocurran a mitad de juego, como por ejemplo la aplicación de filtros a un sonido que se reproducía en un espacio abierto comparado a un espacio cerrado, la implementación de propiedades físicas a un sonido para simular su velocidad real, o modificaciones de tono o volumen para simular efectos como atenuaciones en base a la distancia o Doppler3.

Para facilitar el desarrollo de estos productos finales, las grandes empresas decidieron crear diferentes motores de juego, los cuales incluyen gran cantidad de características tanto en gráficos, audio, interfaz visual de usuario, cinemáticas, físicas y mecánicas, como en manejo de memoria y flujo de código. Gracias a esto, los programadores pueden trabajar de forma mucho más sencilla y crear proyectos mucho más fiables y robustos.

Evidentemente estos ingenieros y profesionales de la materia no solo deben aprender a manejar estos motores nada triviales para empezar, sino que deben manejarse también a niveles más bajos, ya que deben entender en todo momento todo lo que está pasando por debajo y por qué las cosas funcionan como funcionan.

Con respecto al hardware que se utiliza a día de hoy, cabe destacar que el estándar más utilizado es el de la especificación de Intel HD Audio [11]. Aquellos chips basados en este estándar, pueden llegar a ofrecen hasta 16 canales, frecuencias de muestreo entre los 6 y los 192kHz entre otras, pero desde 2008, la mayor parte de este hardware están limitados a 8 canales de salida y 4 de entrada.

2 Efecto sonoro que busca recrear un sonido particular, cuyo método de obtención no se asemeja al sonido original.

3 Efecto donde la percepción de la frecuencia de una onda varía en función de si se aleja o acerca al oyente

(25)

24

4.2.1. Herramientas de Alto Nivel para Sonido

La particularidad del audio es que el software disponible para desarrollo de videojuegos trae embebida la división de los diferentes roles en el desarrollo de audio. Ejemplos de herramientas que se usan en estos proyectos son FMOD, Miles [12] o WWise, teniendo formas de trabajar más o menos común, repartiéndose en una aplicación de estudio y una API para el código, como se puede ver en las figuras 4 y 5.

La aplicación de estudio, busca facilitar la tarea al diseñador de sonido, permitiendo la grabación de los sonidos, mezclado, procesamiento y creación de eventos sonoros de diferentes tipos, que posteriormente podrán ser interpretados y programables en el código, creando una biblioteca de sonidos y eventos parametrizados para el programador.

Figura 4. Interfaz FMOD Studio (Fuente: https://www.fmod.com/)

Figura 5. Interfaz WWise

(Fuente: https://www.audiokinetic.com/products/wwise/)

(26)

25

Posteriormente la API, dará al programador de sonido un kit de desarrollo compatible con todos los materiales anteriormente creados por el diseñador de sonido, que permitirá programar los comportamientos de los sonidos y los eventos en las diferentes situaciones que se planteen en la escena sonora.

Sumando el trabajo conjunto de los dos apartados, se da lugar a herramientas muy completas con gran potencial a la hora de crear situaciones verdaderamente realistas y sorprendentes.

4.3. Desarrollo independiente de videojuegos

El verdadero fenómeno reciente, es el nacimiento del desarrollo de juegos independientes.

Alejándose de los grandes equipos de desarrollo, las grandes cantidades monetarias, las exigencias de guiones y las fechas límites, existe un mundo de usuarios con intenciones de crear sus propios videojuegos que plasmen sus historias, sin que haya otras fuerzas, cargas o presiones que modifiquen esta idea original. La cuestión de este tipo de desarrolladores es que para crear sus proyectos, no tienen por qué ser ingenieros, ni siquiera tener conocimientos de programación en ocasiones.

En lo que incumbe al apartado sonoro, el encargado o encargados del sonido en el juego, siguen teniendo a su disposición las herramientas de alto nivel anteriormente mencionadas, donde si el equipo de trabajo independiente está dividido en programadores y diseñadores, estas herramientas resultan muy acordes a la forma de trabajar.

Sin embargo, en ocasiones estos equipos son mucho más reducidos, de forma que entre pocos se dividen tareas que abarcan muchos más campos o incluso una única persona se dedica al desarrollo del proyecto entero.

Como solución a este problema surgieron los conocidos motores de creación de videojuegos, siendo el más conocido Unity, que proporciona una interfaz de usuario muy accesible para que gente que quiera crear sus videojuegos pueda hacerlo sin tener por qué tener conocimientos de programación. A pesar de ello, muchos equipos de trabajo optan por desarrollar sus proyectos escribiendo código directamente sin usos de otras herramientas entre medio, o al menos en la menor medida posible, quedando a manejo del programador la creación de infraestructura y arquitectura del juego, entidades y flujo general.

Por suerte, para otras tareas, como gráficos o sonido, existen APIs y librerías compatibles con código fuente que amenizan mucho el trabajo, o incluso motores implementables al código

(27)

26

fuente, que manejan correctamente sus propios recursos tanto en aspecto de memoria como de procesamiento, siendo especialmente útil en casos en los que el usuarios no tenga conocimientos sobre teoría de gráficos o sonido. Incluso muchas de estas herramientas no solo son de código abierto, ofreciendo al programador la posibilidad de explorar el funcionamiento interno del motor, sino que muchas veces traen implementadas infraestructuras propias como por ejemplo Irrlicht. Irrlicht es una librería de gráficos, que permite al programador olvidarse de la gestión interna de los componentes del motor, encargándose Irrlicht de los cálculos sobre gráficos y manejo de memoria, y así el programador únicamente se dedica a llamar a las funciones para crear, modificar los componentes gráficos y asociarlos a los componentes de su juego, sin preocuparse del aspecto teórico que conlleva.

4.3.1. Herramientas de Bajo Nivel en Sonido

Para el caso del sonido, el uso de APIs también está bastante considerado, ya que ofrece además un punto a favor para aquellos equipos que no tengan asignados claramente roles de diseñadores y programadores de audio. Como estas librerías no tienen ninguna dependencia a otras aplicaciones de diseño, eliminan la división entre programador y diseñador, pudiendo desde ellas mismas realizar todas las tareas respectivas al audio en un juego.

Algunos ejemplos de APIs de este tipo podrían ser PortAudio [13] o también OpenAl [14], que tienen como principal ventaja frente a las herramientas de alto nivel como FMOD o WWise, que son de código abierto y totalmente gratuitas, por lo que puede resultar más atractiva a equipos que quieran sacar beneficio de sus proyectos pero no tengan un presupuesto de desarrollo especialmente grande.

Sin embargo, existen una serie de desventajas considerables en los dos tipos de herramientas con las que trabajamos que vamos a comentar a continuación.

(28)

27

4.4. Problemática en el audio

4.4.1. Problemas en las herramientas de Alto nivel

Para este tipo de herramientas tenemos motores como los de FMOD o Wwise. La forma de funcionar que tenían era la siguiente: Por un lado, disponen de un aplicación de escritorio, como FMOD Studio, desde la que se pueden crear eventos de audio de diferentes tipos, con multipista, bucles, programables, etc. Creados estos, usamos una API, que será la que se encargará de leer estos eventos, teniendo nosotros que crear la infraestructura del motor en el código del proyecto. Aunque nos pueden facilitar algunas tareas al encargarse internamente del manejo técnico del audio, como se encargan sus APIs, dependen directamente de una aplicación de alto nivel que añade una capa de complejidad más que en muchas ocasiones no es necesaria por las necesidades del proyecto, ya que por un lado se requieren conocimientos de diseño sonoro para la creación de eventos y por otro lado ofrece una cantidad de posibilidades mucho más grande de lo que en ocasiones se necesita de verdad.

4.4.2. Problemas en las herramientas de Bajo Nivel

De la misma forma que hemos comentado de Irrlicht, un motor gráfico con el que no te tienes que preocupar del funcionamiento teórico de los gráficos y simplemente llamas a las funciones y creas los objetos necesarios, no tenemos lo mismo en audio.

Al recurrir a APIs de sonido que no tuvieran dependencias como las anteriores herramientas, como por comentábamos OpenAL o Portaudio, por una parte, consideran que la tarea de sound designer y sound programmer recaen sobre la misma persona. Por otra parte, nos topamos que al ser de bajo nivel, exigen determinados conocimientos al programador relativos al manejo del sonido y de memoria del sistema, ya que tiene que ser él el que se encargue activamente de manejo de los ficheros de audio, reservando y eliminando memoria para no sobrecargar el sistema. Además, el programador tiene que considerar todo el estudio de teoría de audio que conlleva, como comunicaciones con drivers de sonido por un lado, o el manejo de muestras de audio por otro.

(29)

28

4.5. Propuesta

Por estas razones propongo la creación de un motor de sonido, de código abierto en C++, dado que es el lenguaje más común entre el tipo de programadores al que va orientado, que se encargue del trabajo interno del sonido, del manejo de memoria y otras cuestiones de bajo nivel, el cual permita al usuario implementar audio en su sistema de forma sencilla sin por un lado tener dependencias de aplicaciones externas, ni por otro lado obligar al usuario a complicarse al tener que crear funciones que computen el sonido directamente. Será tan sencillo como que el usuario cree sus objetos de audio, que tendrán diferentes propiedades, como si es mono o estéreo, su posición en el espacio, volumen, panorama, si se reproduce en bucle, etc… Y ya el propio motor se encargará de todo el procesamiento y manejo del sonido, dando la opción como decíamos, al ser de código abierto, de explorar internamente el funcionamiento del mismo para comprender mejor el procesamiento de ficheros de sonido a bajo nivel.

(30)

29

5. Objetivos

Definir de forma clara cuáles son los objetivos de nuestro proyecto nos será de gran ayuda para definir los puntos cruciales que determinan si el proyecto ha sido exitoso o no.

Determinemos estos objetivos primero de forma general. que tienen que ver directamente con el proyecto.

Para el tipo de proyecto en el que vamos a trabajar, nuestro principal objetivo es el de la creación de un motor de sonido útil, capaz de manejar eventos sonoros en aplicaciones no lineales. El fin ideal del proyecto sería visto de forma en la que un usuario que ha trabajado en un producto del tipo de un videojuego, sea capaz de, sin modificar la infraestructura del mismo, simplemente dando nuevos atributos a sus entidades y llamando a las funciones pertinentes, pueda tener sonido en su juego, siendo modificable a distintos niveles.

A pesar de que es bien sabido que existen en el mercado varias opciones al respecto de manejo de audio en código que son de mayor nivel y con una cantidad más grande de funcionalidades, con el desarrollo de este motor tenemos como prioridad que sea una alternativa útil, ligera y de bajo coste, que a pesar de no tener todas las funcionalidades que tienen otros competidores, sirva para evitar los quebraderos de cabeza que nos suponen en ocasiones la implementación de motores más grandes.

Dado que el motor está pensado para poder ser implementado en cualquier proyecto de estas características, tendremos que desarrollar un código capaz de actuar lo más independientemente posible al proyecto al que se conecta, y por ello, tener cuantas menos dependencias a librerías y código ajeno posible. Adicionalmente, se tiene como prioridad que esté hecho de forma comprensible y accesible, dado que tenemos como sub-objetivo que este motor sirva además como herramienta de aprendizaje y ayuda al programador a comprender cómo se trata el audio tanto en el manejo de su variable en memoria como en su procesamiento de lectura y otras propiedades de audio puras.

Para hacer esto alcanzable, todo el proyecto será de código abierto, dando la función a aquellos usuarios que lo deseen, de adentrarse en las funciones del motor para comprender todas estas características. Es considerable también que no se tiene ninguna intención monetaria con el desarrollo de este proyecto ya que como decíamos, se tiene antes una prioridad de servir y enseñar antes que de obtener ingresos.

(31)

30

6. Metodología

Primeramente, centrándonos en el desarrollo de este motor, considero que la mejor forma de encauzar su elaboración es con una metodología Scrum enfocada al trabajo individual, donde al no trabajar con un equipo, las reuniones de grupo quedan apartadas, pero se sigue el concepto de sprints cortos. Considero que es la más apropiada por varias razones.

Primeramente, es una forma de trabajar con la que ya estoy familiarizado, ya que en anteriores proyectos de características similares se optó por esta forma de funcionar obteniendo buenos resultados. Escojo esta metodología también porque se ha avalado en repetidas ocasiones que es una estrategia de desarrollo fiable que da frutos, además de que no se hace nada complicado trabajar con ella, ya que al enfocarse a objetivos a corto plazo, la sensación de progreso es mayor que si se pusieran objetivos mucho más grandes aunque sean a mayor plazo. Cierto es que se pierde el apartado de las reuniones con miembros del equipo, natural del Scrum, por motivos evidentes

Por otro lado, para el desarrollo de la memoria se optará por metodología Kanban, similar a Scrum, pero sin considerar sprints de trabajo cortos, sino un flujo de trabajo continuo.

Más concretamente, tomaremos como posibles estados de tareas, tanto para la memoria como el código del motor en “Pendiente”, “En Progreso” “Hecho por revisar” y “Hecho y revisado”.

Para el motor, trabajaremos en sprints de dos semanas, que es un tiempo suficiente para dejar las tareas implementadas y pulidas de la mejor manera posible para pasar a nuevas tareas sin dejar trabajos pendientes.

Para el manejo de las tareas se hará uso de Trello [15], un sitio web de gestión de proyectos donde agrupar las tareas en diferentes categorías para facilitar su seguimiento

(32)

31

Los tableros se diferenciarán en que para el código del proyecto, las tareas se dividirán en sprints.

En la figura 6, vemos un ejemplo de progreso con respecto a la redacción de la memoria. Cabe aclarar que en la columna “Hecho y revisado”, se agruparán aquellas tareas finalizadas y con el visto bueno del tutor.

Figura 6. Estado de tareas en Trello (Fuente propia)

(33)

32

7. Análisis y especificación

Mediante un análisis de los requerimientos del proyecto, vamos a canalizar las necesidades que va a tener el trabajo para cumplir su objetivo satisfactoriamente

Trabajaremos con requerimientos de dos tipos, funcionales y no funcionales. A su vez, dividiremos los requisitos funcionales en tareas base y componentes. Las tareas base serán cruciales para el funcionamiento de la aplicación, y las tareas componentes se irán implementando para dar riqueza al motor.

Los requerimientos funcionales base los llamaremos RFB, y los relativos a componentes los llamaremos RFC. Los no funcionales por último los llamaremos RNF.

7.1. Requerimientos funcionales base

RFB1: Creación de la infraestructura del motor. Lo primero y más básico será la creación de lo que son los métodos básicos y del flujo que tendrá el audio en el proyecto. Será una tarea que podrá estar en constante desarrollo, ya que la arquitectura podrá modificar en mayor o menor medida dependiendo de lo que se vaya necesitando. Tendrá en cuenta el ciclo de vida tanto del motor, en su creación y destrucción, al igual que el de los objetos de audio con los que trabaja y los métodos que alberga.

RFB2: Lectura y reproducción de ficheros de audio. En nuestro caso, el principal objetivo será el de ser capaces de reproducir sin errores un fichero de audio. Por tanto tendremos que preparar el código para que sea capaz de leer este tipo de ficheros y que tome las acciones convenientes con su tipo en lo relativo al sonido.

RFB3: Manejo de memoria de variables de audio. Cuando trabajamos con este tipo de ficheros, tenemos que ser capaces de administrar la memoria correctamente, ya que tendremos que dotar a los ficheros de canales para su reproducción, asociarlos a objetos con diferentes variables para que se reproduzcan de forma correcta, y crearlos y eliminarlos convenientemente para optimizar el código lo máximo posible.

(34)

33

7.2. Requerimientos funcionales de tipo componentes

RFC1: Reproducción simultánea de varios audios. El motor no debería ser capaz de reproducir un único fichero de audio por vez, sino poder reproducir simultáneamente cuantos audios sean, sin importar su momento de inicio o fin.

RFC2: Volumen modificable. Nos interesa que los sonidos que se reproduzcan tengan un volumen modificable y variable tanto previo a, cómo durante su reproducción.

RFC3: Lectura de fichero en mono y estéreo. Según el tipo de wav que se reciba, se deberá procesar de una forma u otra en base a los canales que tenga, ya que nos servirá para posteriormente utilizar esto a nuestro favor para otros requerimientos.

RFC4: Panorama. Además de ser capaces de distinguir ficheros mono y estéreo, tendremos en el código que ser capaces de modificar el panorama de los ficheros, pudiendo así crear la ilusión de movimiento al oyente.

RFC5: Bucles. Deberemos de poder implementar audios en bucle modificables en el motor, para lograr situaciones como música de fondo o atmósferas ambientales.

RFC6: Posicionamiento entorno 3D. Viniendo de la mano de la panoramización y del volumen, nuestro objetivo aquí será el de, dada la posición en el espacio de una fuente de audio, ser capaces de situarlo en el espacio, dotándolo según normas físicas para mayor realismo, de un volumen y panorama variable para lograr situar en el espacio las fuentes de audio consiguiendo una experiencia de usuario mucho más inmersiva.

RFC7: Optimización en carga y descarga de ficheros. A la hora de la reproducción de múltiples objetos de audio que contienen el mismo fichero en sí, sería conveniente evitar que se carguen múltiples instancias iguales que estén cargando y descargando de memoria constantemente.

(35)

34

7.3. Requerimientos No Funcionales

RNF1: Accesible al usuario. Dado que teníamos como objetivo que fuera una herramienta a su vez didáctica, buscaremos que el motor sea fácil de entender, teniendo el usuario (en este caso, un programador) acceso directo al código para recurrir a las funciones tanto las más cercanas al código y manejo de ficheros como a los de lectura pura del audio.

RNF2: Ligero sin dependencias. También teníamos como objetivo que fuese ligero y que estuviera optimizado cuanto más posible. Para ello, intentaremos que tenga cuantas menos dependencias posibles para evitar complicaciones.

RNF3: Fácil de implementar. Con esto queremos que el usuario, simplemente conectando mediante código el motor a su proyecto, ya pueda reproducir ficheros de audio en un entorno de juego.

RNF4: Sistema de manejo de errores. En caso de que ocurra algún problema durante la ejecución del código, se tendrá contemplado mediante un manejador de errores.

(36)

35

8. Diseño

8.1. Planteamiento inicial

Definamos primeramente como queremos que trabaje nuestro motor para tener una idea general de su funcionamiento. Planteemos el que sería el ciclo de vida de un fichero de audio en el motor.

Los pasos serán: Primeramente, desde el código del usuario, se determina que una variable de audio se quiere reproducir, por tanto se crea una instancia del mismo en el motor. En este momento se lee el fichero, se le reserva espacio en memoria y se graban sus metadatos en las variables.

Posteriormente, si el fichero quiere ser reproducido, al entrar en el bucle del motor de sonido, se le dará un canal, se le asignará variables de volumen y panorama en función de su posicionamiento en el juego y se pasará a reproducir en base a estos parámetros. Cuando el audio termina, si está en bucle se vuelve a comenzar. De no ser el caso, se libera el canal y vacía la reserva de memoria del fichero, dejándolo libre para uso de otros.

Para lograr llevar a cabo este concepto, vamos a desarrollar el motor en forma de un sistema monolítico [16]. En este tipo de sistemas, todas las funcionalidades y utilidades se integran al mismo proceso, trabajando en lo que sería un único servicio. Dada la naturaleza del motor y el tipo de datos con los que se va a manejar, que van a ser similares a la hora de su tratamiento, el sistema monolítico nos va a ayudar a mantener una estructura más firme en el proyecto y ahorrarnos en complejidad.

Planteada la visión general, lo siguiente será crear una estructura base sobre la que funcionará todo y crear todas los objetos que vayamos a necesitar. Antes de desarrollar métodos y funciones, debemos de preguntarnos: ¿Cuáles son los elementos que intervienen en el entorno del audio, y que tendremos que simular en la aplicación?

(37)

36

Primeramente, una fuente de audio, que llamaremos AudioFile. Este objeto será el que contendrá todos las variables que se puedan asociar al audio: Primero un identificador para distinguirlo, la ruta del fichero donde se aloje el audio y metadatos del formato de audio que nos serán más útiles a la hora de su lectura, como frecuencia de muestreo original, cantidad de muestras, equivalente a la duración del fichero, o si es mono o estéreo, y muy importante el canal que ocupará durante su reproducción.

Los canales son una representación de datos de sonido que puede manejar un dispositivo de audio manejado por las APIs de audio nativas de la máquina, cuyo número es limitado y debe ser manejado correctamente si se quiere tener control sobre los recursos que se trabajan. Cada vez que se pone a reproducir un sonido cargado por fichero, la API que maneje el sonido en la máquina será la encargada de reservar un canal físico en el hardware de sonido, para que la señal digital pueda convertirse a señal analógica para su posterior reproducción.

Por ejemplo, en el caso de Linux, se trabaja con ALSA [17] (Advanced Linux Sound Architecture) como API de manejo de sonido. En el momento que una aplicación pide reproducir un sonido, es ALSA quién se comunica con el kernel4 para reservar un canal físico en la tarjeta de sonido y poder hacer el playback [18]. En la figura 7, tenemos un ejemplo visual de cómo se comunican los diferentes elementos.

4 Elemento del sistema operativo que se encarga de conceder el acceso al hardware de forma segura para todo el software que lo solicita,

(38)

37

Figura 7. Esquema de funcionamiento de ALSA

(Fuente: https://github.com/voice-engine/make-a-smart-speaker/issues/5)

También necesitaremos darle variables parametrizables después en el código y en el durante de la ejecución del programa, como volumen tanto general como por canales, como posición en el entorno. Por último, usaremos variables auxiliares para ayudarnos con su procesamiento, como su estado (si está pausado, reproduciéndose o listo para reproducirse), además de variables End of File o si está en bucle (loop). El siguiente elemento será el usuario propiamente dicho situado en el entorno del juego, que lo llamaremos Listener. El Listener se determinará por dos variables:

La posición en la que se encuentra y un vector que determinará la dirección a la que está orientado, que será importante a la hora de simular la percepción del sonido en el usuario en función de donde mire. Por último, un juego de altavoces que llamaremos SpeakerPair. Nos serán cruciales a la hora de determinar cualidades como el panorama de los sonidos, y tendrá como variables su posición relativa al listener y el ángulo que abarca cada uno, es decir, que si una fuente de sonido sobrepasa determinado ángulo, no se escuchará por ese altavoz. Todos estos elementos y sus relaciones las podemos ver reflejadas en la figura 8.

(39)

38

Figura 8. Esquema de elementos involucrados en el motor (Fuente propia)

8.2. Herramientas que se usarán

Concebido el planteamiento inicial, toca cuestionarse qué tipo de tecnologías vamos a utilizar, ya que como vamos a trabajar con audio a bajo nivel, necesitaremos algún tipo de herramienta que nos ayude a manejar ficheros de sonido y conectar con los drivers de sonido. Vamos a ver qué librerías vamos a usar

(40)

39

8.2.1. PortAudio

PortAudio es una librería de código abierto escrita en C que permite escribir programas relacionados con el manejo de audio compilables en plataformas tanto de Windows, como de Linux o Mac. El objetivo que tiene es el de ofrecer una comunicación entre la aplicación del usuario y las APIs de audio específicas de la plataforma en la que se trabaje, de forma que las llamadas a la API de PortAudio que se hagan en código se convertirán a llamadas particulares de las APIs nativas. En la figura 9, se puede ver esquematizado el funcionamiento de PortAudio.

Figura 9. Esquema de funcionamiento de PortAudio

(Fuente: http://files.portaudio.com/docs/v19-doxydocs/api_overview.html)

PortAudio contempla en su modelo tres abstracciones principales que serán manejadas de una forma u otra en el proceso de ejecución del código. Primeramente la “Host API”, que será la API particular del sistema operativo en el que se está trabajando, con la que PortAudio se comunicará. Segundo el “Device”, que será el hardware de la máquina que recibirá la información final de audio. Por último, los “Streams”.

Los streams, ligados estrechamente a los canales son la representación del flujo de audio en el programa, o lo que es lo mismo, representarán los canales de audio manejables en el motor.

Cada uno tiene asociado sus propias características relacionadas con el audio, como su frecuencia de muestreo, tamaño de buffer5 o latencia. Sobre estos streams, PortAudio tiene dos formas de leerlos y procesarlos, pero aquí vamos a tratar la principal que será la que utilizaremos: los callbacks.

5 Muestras totales que serán procesadas en dicho stream por cada iteración

(41)

40

Un callback es una llamada asíncrona que se ejecuta periódicamente con el objetivo de procesar el stream. Dentro de esta función es donde se trabajará con los parámetros del audio que le habíamos pasado por el stream (frecuencia de muestreo, tamaño de buffer, etc) y se tratarán además otros como su amplitud, sus canales o su panorama, procesando en cada iteración una cantidad de muestras del audio en cuestión.

Pero claro, el problema de todo esto, es que PortAudio de forma nativa está preparado para procesar ondas generadas por la misma API o ruidos de diferentes tipos, pero no archivos de audio con formatos específicos. Por tanto necesitaremos algún tipo de librería que nos ayude a leer ficheros de audio formateados que sean procesables por las funciones de PortAudio. Aquí es cuando entra en juego Libsndfile [19].

8.2.2. Libsndfile

Esta librería permite cargar en el código fichero de audio, tanto para lectura como para escritura, en diferentes formatos. Cuando carguemos en memoria un fichero de audio, por ejemplo un .wav, vendrá asociado con sus metadatos, como su frecuencia de muestreo y su cantidad total de muestras, además de ser leíble a través de estas muestras. Por tanto, en principio es ideal para manejarlo a través del callback de PortAudio, ya que al trabajar con muestras de sonido, lo único que tendremos que hacer es procesarlas en el callback con sus parámetros correspondientes.

Con todo esto planteado, vamos a ver como sería el desarrollo total del motor.

8.3. Desarrollo y diseño de los métodos

Para comenzar, vamos a tener claro el entorno en el que vamos a trabajar. Crearemos el motor para que opere en Linux, dado que las librerías con las que vamos a trabajar están mejor preparadas para esta plataforma. Además lo escribiremos en C + +, dado que las librerías están disponibles para este lenguaje.

Primeramente, volvamos a considerar los elementos que se involucrarán en el motor y cómo los vamos a adaptar a nuestras nuevas librerías, especialmente el AudioFile, el objeto básico del motor sobre el que se hacen la mayoría de las operaciones. La principal novedad que tenemos aquí es la inclusión de la librería Libsndfile, ya que dotaremos a cada objeto de dos variables nuevas: una nos permitirá referenciar a lo que será la variable del audio en sí, más la posibilidad de guardar y leer los diferentes datos del fichero, como su formato, canales, frecuencia de muestreo, etc. A su vez, podemos dotar al audio de su propio puntero a canal, que será el Stream

(42)

41

que abrirá PortAudio para reproducir en el callback, funcionalidad que veremos más adelante, ya que se podrá tratar de dos formas diferentes. Por ahora, dotaremos a cada AudioFile de su propio Stream.

Con esto visto, comenzamos con el ciclo de vida de un fichero de audio, primeramente de la forma más básica: Cargar un fichero, reproducirlo y eliminarlo al terminar su lectura.

8.3.1. Inicialización y procesamiento básico

Para empezar, al comienzo inicializamos la librería de PortAudio y las variables de SpeakerPair, container (almacén de AudioFiles) y listener, creando una instancia para cada una de ellas, dándoles valores por defecto que ya podremos modificar más adelante.

Posteriormente, vamos a crear el método para carga de audios, que recibirá como parámetro la ruta en el que se encuentra el audio que se va a cargar, y creará un objeto AudioFile que guardaremos. Dentro del método, reservamos un espacio para el nuevo fichero, rellenamos con los valores por defecto que les queremos dar en el motor, como sus volúmenes y panoramas a un estado inicial, su id, o su estado a “En pausa”. A través del path que le hemos pasado, se llama a la función de lectura propia de la librería Libsndfile, que lee la ruta del fichero, reserva el audio en sí en memoria, y se vuelca en la variable de información, todos los metadatos relativos al fichero.

Hecha lo que sería la inicialización del fichero, vamos a proceder a crear el bucle básico del motor y como acabaría una ejecución.

Crearemos un método update, que se llamará en bucle en el código del juego en el que esté implementado. Dentro, se llamará a dos funciones: por un lado, una que reproducirá los audios y otra por otro lado, que borrará de memoria aquellos objetos cuya reproducción haya finalizado. Vamos a estudiar la función de reproducción de ficheros, ya que es importante entender cómo funciona el playback de los ficheros en un entorno como PortAudio.

Cuando entramos en esta función, se carga el audio por parámetro, cuyo estado debe de ser

“Listo para reproducir” si queremos que suene, estado que habrá sido activado por alguna acción en el juego, como por ejemplo, durante el evento de un disparo, donde se pondría esta variable activa. De ser el caso, procedemos a abrir un stream de PortAudio. En la función para abrirlo, le pasamos por parámetro el número de canales de audio que va a tener, frecuencia de muestreo, tamaño de buffer, el archivo que va a leer y el callback que va a realizar las operaciones de audio pertinentes. Acto seguido tras comprobar que no da ningún error,

(43)

42

lanzamos el stream para que comience su reproducción, y actualizamos variables de control como el estado a “Reproduciendo” o que tiene un activo asociado.

Ahora viene un concepto muy importante a la hora de entender el funcionamiento de PortAudio. Una vez se ha iniciado la reproducción del stream de un fichero, y su canal tiene una función callback asociada, esta se llamará asíncronamente sin que nosotros la tengamos que llamar activamente, hasta que se cierre el stream determinado cuando haya finalizado. Dicho esto, veamos cómo funciona una llamada callback.

8.3.1.1. Funcionamiento de un Callback

El callback en sí tiene dos buffers manejables, uno de entrada/input y otro de salida/output. Por la naturaleza de nuestro motor solo usaremos el segundo, que será el buffer cuyos valores serán los que se enviarán a las APIs de audio de la máquina para generar el sonido.

El concepto inicial del callback, consiste en que partimos de un audio compuesto de muestras digitales. En cada iteración, el callback reproducirá una cantidad de muestras determinada, dada por nosotros y que llamaremos framesPerBuffer, multiplicada por los canales propios del fichero. Si es un fichero estéreo, tendremos dos ondas diferentes que tendremos que procesar a la vez, y por tanto multiplicamos por dos el valor resultante.

Con las muestras de audio preparadas para su lectura, y otro espacio reservado para las muestras que vayan a salir, tenemos que comprobar si los frames que vamos a leer en esta ocasión, sumados a los frames que hubiéramos leído en iteraciones anteriores, acaban superando el total de muestras del fichero, y de ser el caso, lo reducimos hasta que quede cubriendo solo hasta la última, ya que si no estaríamos procesando valores irrelevantes para el resultado del algoritmo. Ahora se ejecutará el bucle de lectura tantas veces como muestras fuéramos a procesar (framesPerBuffer multiplicado por los canales). Esto es especialmente importante para cuando trabajemos con ficheros de audio en mono o estéreo.

Para procesar el fichero según los canales, la forma más sencilla e inicial es en su versión mono.

Cuando guardamos las muestras de un audio, se almacenan de forma ordenada al haber solo un canal: La muestra 0 de nuestra reserva es la muestra 0 del fichero. La 1, la 1 y así sucesivamente.

Cada una de estas muestras se asocia a la misma posición en el array del outputBuffer, el array de muestras ya procesadas de salida, que se reproducirá en el mismo orden. En caso de ser un fichero estéreo la situación cambia como se ve en la figura 10. Al haber dos señales de audio diferentes, se guardan alternativamente, es decir, la muestra 0 de nuestra reserva es la muestra 0 del fichero del canal izquierdo, y la muestra 1 de nuestra reserva es la muestra 0 del fichero

(44)

43

del canal derecho. Por esto multiplicábamos antes los framesPerBuffer por los canales, ya que dependiendo del fichero, para una misma cantidad de tiempo vamos tener más señales que procesar a la vez. En definitiva, tenemos que considerar que el array guardará en las posiciones pares las muestras del canal izquierdo y en las impares las del canal derecho. Sabiendo esto, actuamos en consecuencia, asociando a esas mismas posiciones del outputBuffer.

Figura 10. Redistribución de muestras en nuevo buffer (Fuente propia)

Como detalle, es importante que podamos procesar un fichero mono como estéreo, ya que cuando queramos modificar el panorama de este, a pesar de tener solo una señal de datos, tendremos que generar dos diferentes para los altavoces izquierdo y derecho. Para esto, lo que vamos a hacer es asociar la misma muestra de audio de la señal mono, a la señal par e impar del outputBuffer, de forma que tengamos dos señales iguales, pero con la posibilidad de modificarlas independientemente una de otra.

Este bucle se ejecuta una vez por muestra o par de muestras, dependiendo del formato del fichero, tantas veces como muestras por buffer tengamos, hasta llenar el outputBuffer. Una vez lleno, actualizamos las variables de control, como la cantidad de frames leídos frente a los totales y comprobamos si el audio ha finalizado.

Si durante la ejecución en el update, se ha llegado al fin del fichero, cuando vayamos a comprobar el estado de los AudioFiles, se para y cierra su stream y se limpian sus variables, borrando sus ocupaciones y dejando espacio para otros ficheros en el container de audios. En la figura 11 se puede ver esquematizado lo que sería el flujo desde el arranque hasta la finalización del motor.

(45)

44

Figura 11. Esquema básico del motor de sonido (Fuente propia)

(46)

45

Con esto ya estaría el procesamiento base para un fichero de audio único, y con esto cubriremos los 3 requerimientos funcionales base, RFB1, RFB2 y RFB3; y la funcionalidad RFB3. Ya tenemos una infraestructura básica para el manejo de audios, que iremos modificando y ampliando a partir de ahora, somos capaces de leerlo y reproducirlo correctamente, y cargarlo y eliminarlo cuando y como es debido, además de poder hacerlo para fichero tanto mono como estéreo.

Pero un motor de audio no se reduce a un simple reproductor de audio. Debemos tener más funcionalidades para conseguir una experiencia mejor para el usuario y lograr un resultado realista para que el juego al que se implemente sea mucho más fiel. Para empezar vamos a cubrir el RFC1, la reproducción simultánea de varios audios:

8.3.2. Reproducción simultánea de varios audios

Partimos de tener un método capaz de leer un fichero de audio particular, pero claro, ahora debemos de ver de qué manera procesamos varios audios a la vez. Aquí existen un par de formas que ya sugerimos anteriormente que vienen estrechamente relacionadas con el manejo de los streams de PortAudio. Vamos a ver qué dos vías tenemos, y posteriormente veremos con cual nos quedaremos y por qué.

La primera forma que tenemos es la siguiente. Como habíamos comentado antes, ahora mismo cada objeto AudioFile tiene stream, pero esto puede no hacerse así. En lugar de trabajar con un stream por objeto, vamos a pensar cómo hacerlo con un único stream para todos los audios.

Dado que por cada stream abrimos una llamada al callback, lo que vamos a hacer en el bucle update será: En lugar de abrir un canal por defecto pasándole por parámetro un único audio, vamos a pasarle todos los audios cargados en ese momento. Ahora dentro del callback lo que haremos será antes de procesar las muestras, recorremos los ficheros y vemos si su variable de estado dice si está listo para reproducción. De ser el caso, hacemos el mismo procesamiento que antes, cogiendo muestra a muestra y guardando en el outputBuffer (recordemos, el array de salida que contiene las muestras procesadas listas para sonar) para su posterior reproducción. Pero aquí entra el cambio. Cuando terminemos de leer un fichero y vayamos a leer otro que también está pensado para leerse, lo que vamos a hacer es al mismo outputBuffer sobre el que estábamos volcando las muestras del primer audio, vamos a sumarle las muestras del siguiente. Como el audio con el que estamos trabajando está digitalizado, sabemos por teoría de señales que si sumamos dos muestras de dos señales diferentes, en este caso de sonido, la

Referencias

Documento similar

En suma, la búsqueda de la máxima expansión de la libertad de enseñanza y la eliminación del monopolio estatal para convertir a la educación en una función de la

Where possible, the EU IG and more specifically the data fields and associated business rules present in Chapter 2 –Data elements for the electronic submission of information

The 'On-boarding of users to Substance, Product, Organisation and Referentials (SPOR) data services' document must be considered the reference guidance, as this document includes the

In medicinal products containing more than one manufactured item (e.g., contraceptive having different strengths and fixed dose combination as part of the same medicinal

Products Management Services (PMS) - Implementation of International Organization for Standardization (ISO) standards for the identification of medicinal products (IDMP) in

Products Management Services (PMS) - Implementation of International Organization for Standardization (ISO) standards for the identification of medicinal products (IDMP) in

This section provides guidance with examples on encoding medicinal product packaging information, together with the relationship between Pack Size, Package Item (container)

Package Item (Container) Type : Vial (100000073563) Quantity Operator: equal to (100000000049) Package Item (Container) Quantity : 1 Material : Glass type I (200000003204)