Download Programación en Java desde la perspectiva del audio
Document related concepts
no text concepts found
Transcript
Programación en Java desde la perspectiva del audio PROGRAMACIÓN EN JAVA DESDE LA PERSPECTIVA DEL AUDIO JUAN DAVID LOPERA RIBERO PONTIFICIA UNIVERSIDAD JAVERIANA FACULTAD DE ARTES CARRERA DE ESTUDIOS MUSICALES - INGENIERÍA DE SONIDO BOGOTÁ 2010 2 TABLA DE CONTENIDOS Preliminar Introducción........................................................................................................................................ 1 Antes de empezar ............................................................................................................................... 7 NetBeans IDE .................................................................................................................................... 18 Bases del lenguaje Anatomía de Java ............................................................................................................................. 24 Variables ........................................................................................................................................... 30 Comentarios...................................................................................................................................... 35 Tipos de Variables............................................................................................................................. 37 Arreglos ............................................................................................................................................. 42 Matemáticas ..................................................................................................................................... 45 Sentencias de prueba 'if' .................................................................................................................. 51 Ciclos ................................................................................................................................................. 57 Métodos ............................................................................................................................................ 63 Ámbitos locales................................................................................................................................. 71 Conversión de tipos .......................................................................................................................... 74 Los Objetos ¿Qué son los objetos? ...................................................................................................................... 78 Encapsulación ................................................................................................................................... 84 Herencia ............................................................................................................................................ 93 i Polimorfismo .................................................................................................................................. 101 Clases externas ............................................................................................................................... 105 Más allá de las bases Excepciones..................................................................................................................................... 111 Multihilos ........................................................................................................................................ 116 Estáticos .......................................................................................................................................... 119 ¿Qué es un API? .............................................................................................................................. 122 GUI .................................................................................................................................................. 127 Eventos............................................................................................................................................ 146 MIDI API Números binarios, decimales y Qué es MIDI ................................................................................. 152 La comunicación MIDI .................................................................................................................... 157 La información MIDI ....................................................................................................................... 170 Bancos de sonidos .......................................................................................................................... 183 Archivos MIDI ................................................................................................................................. 189 Edición de secuencias ..................................................................................................................... 192 Sampled API Teoría de audio digital .................................................................................................................... 195 Explorando los recursos del sistema .............................................................................................. 207 Capturar, grabar y reproducir ........................................................................................................ 219 ii Programación de un metrónomo Una aplicación real ......................................................................................................................... 226 Planeación....................................................................................................................................... 228 Programando .................................................................................................................................. 232 Resultado y código completo ......................................................................................................... 249 Final Conclusiones ................................................................................................................................... 268 Bibliografía ...................................................................................................................................... 273 iii Introducción Como ingeniero de sonido me he encontrado en situaciones en las que pienso que sería muy útil un software que cumpliera cierta función para alguna necesidad particular. Como músico me ha pasado muchas veces también. Más importante todavía, he visto que estos pensamientos les ocurren a la mayoría de ingenieros de sonido. Buscar un programador que realice exactamente lo que necesitamos no es fácil y además es muy costoso. Aunque sé que programar y diseñar un software no es para todo el mundo, si creo que todo ingeniero, no sólo los de sonido, deben al menos tener bases sólidas en programación ya que esto permite desarrollar una forma de pensamiento diferente y permite crear por uno mismo soluciones a necesidades diarias que explotan nuevas formas de negocio. Hoy día es necesario demostrar que no todo músico tiene que ser pobre, debemos derrumbar la idea que solo se puede triunfar en el escenario o que para sobrevivir como músico e ingeniero de sonido la única posibilidad que tenemos es ser profesores. Es necesario encontrar nuevos nichos de mercado y esto sólo se puede lograr con creatividad y nuevos conocimientos. Aunque este escrito no pretende enseñar ni demostrar cómo hacerse rico con la programación de aplicaciones en Java, si quiero aclarar que me siento orgulloso de poder presentar de una forma clara y ordenada una introducción a lo que creo que es el futuro para muchos ingenieros de sonido que son capaces de crear sus propias herramientas de trabajo y contribuyen en nuestro mundo con soluciones creativas. Quiero contar cómo terminé involucrado en el mundo de la programación, esto para explicar por qué enfoco este proyecto de grado en la programación en Java y no en otro lenguaje de programación. A comienzos de 2008 me interesé por encontrar nuevos sitios de trabajo y terminé ocupado en la producción de audio para páginas web. 1 Al estar en este mundo terminé por conocer al amigo número uno del audio en el mundo web, estoy hablando de Adobe Flash. Hoy día el 95% de los computadores en el mundo tienen instalado Flash Player (http://riastats.com/, 2010) que es el componente necesario en un navegador para ver contenido creado en Adobe Flash. Este programa se ha hecho muy popular gracias a las facilidades que brinda a los diseñadores para crear contenidos ricos visualmente, permite crear animaciones fácilmente, agrega nuevas formas de interacción con el usuario, permite crear juegos y además agregar audio es muy fácil. En la gran mayoría de sitos web a los que entramos que manejan audio, este proceso está siendo posible gracias a Flash. A finales de 2008 descubrí que a la gente le gusta aprender sin salir de casa, no sólo hablo del profesor a domicilio sino de las comodidades que tiene aprender en internet. Decidí crear una academia de música online que enseña con videos, juegos y foros. La mejor forma para poder crear esta página1 era usar Flash de Adobe para manejar los videos, para crear los juegos y para incrustar el audio en los botones. Para empezar a desarrollar los juegos aprendí que Flash usa un lenguaje llamado ActionScript 3.0 que es el lenguaje que nos permite agregar interacción a las aplicaciones. Después de lanzar www.ladomicilio.com en 2009, decidí que quería hacer un metrónomo que la gente pudiera usar en mi página sin necesidad de descargarlo. Empecé a desarrollarlo y en medio del proceso me di cuenta que mi metrónomo no era preciso en el tiempo, nada más frustrante y más dañino que una página que enseña música con un metrónomo que funciona mal. Al comienzo pensé que era culpa de mi inexperiencia programando, pero después de aprender más, preguntar a expertos y ver otros metrónomos online, entendí que Flash NO ES preciso en el tiempo. Entre mis planes de trabajo también quería agregar un juego que permitiera al usuario tocar un ritmo con el teclado del computador y el programa detectaría las imprecisiones rítmicas del usuario. Como Flash no puede cumplir estas tareas, me vi en la obligación de buscar otros medios. 1 La.Do.Mi.Cilio es el nombre de la empresa creada a partir de esta idea. www.ladomicilio.com 2 Debo aclarar que Flash es un excelente programa con muchas posibilidades. Si hoy me piden crear un juego o una aplicación con música que no requiera precisión exacta en el tiempo como un reproductor, un juego de seleccionar la respuesta correcta o cualquier otro parecido, no dudo en usar Flash y ActionScript 3.0. Pero cuando la situación se vuelve un poco más exigente como un metrónomo y un juego de precisión rítmica, debo encontrar otra solución. Desafortunadamente después de esto descubrí que había muchas otras tareas que Flash no podía hacer para nosotros los músicos e ingenieros de sonido. Por ejemplo no podemos manipular los archivos de audio de forma extensiva, no podemos trabajar con MIDI, Flash no soporta ningún archivo de audio en WAVE o AIFF, solamente mp3 y AAC. Investigando aprendí que el lenguaje de programación que era capaz de resolver todos mis problemas y necesidades era Java. Debo aclarar que aunque Java está presente en el 70% de los computadores según las estadísticas de http://riastats.com/ y Flash Player está instalado casi en el 95% de los computadores, esta desventaja no es un problema grande, sólo hay que tenerla en cuenta y saber que instalar Java es extremadamente fácil y es gratis, además estas estadísticas no incluyen la cantidad de dispositivos móviles como celulares que permiten compatibilidad con Java. Hoy día es casi imposible pensar que se está haciendo un trabajo único. Somos en el mundo más de seis mil millones de personas y aunque los medios nos permiten conocer gran cantidad de eventos que están ocurriendo a medio planeta de distancia, es muy difícil saber exactamente cuántos trabajos de este mismo tipo se están desarrollando a esta misma hora o incluso cuántos ya salieron a la luz pública pero no han tenido la suerte de dar con los efectos de la globalización. Publicaciones, trabajos, monografías, libros, revistas y páginas en internet sobre Java hay cantidades inimaginables, pero estos mismos tipos de trabajo que hablen 3 claro sobre Java y su relación con el audio son muy pocos. Muchos de estos textos no solo son aburridos sino que son muy difíciles de entender ya que dan por entendido que uno tiene alguna experiencia en programación. Una vez aprendemos a programar o tenemos algún tipo de experiencia en un lenguaje, es más fácil aprender otro ya que la idea básica es muy parecida de un lenguaje a otro y lo que termina cambiando es la sintaxis. En este proyecto de grado pretendo enseñar a usar Java para que la persona que lea este trabajo esté en la capacidad de crear aplicaciones básicas de audio. Me basaré principalmente en un libro llamado Head First Java segunda edición de la editorial O'Reilly Media que leí de comienzo a final y que a pesar de sus 722 páginas, es muy agradable de leer y además es un libro que entiende la importancia de enseñar de forma divertida y diferente. Además de lo anterior, tiene un capítulo dedicado a MIDI. Recomiendo este libro para todo el que quiera entender Java. Aclaro que hay que tener un mínimo de experiencia en programación para leerlo. Un lenguaje de programación como Java nos permite crear casi cualquier aplicación que imaginemos, esto significa que no pretendo ni puedo enseñarles a diseñar todo lo que se puede hacer en audio con Java. Por ejemplo pensemos en un editor de audio como Pro Tools, Logic, Sonar o alguno semejante. Un software como estos puede ser creado en Java2 pero imaginen todo el equipo de producción que puede requerir crear tal software. Por lo tanto está fuera de los límites profundizar a semejante nivel en un proyecto de grado, pero si es posible obtener unas bases sólidas para empezar a programar en Java que le permitan a la persona que lea este escrito profundizar en el tema que más le interese y gracias a estas bases estoy seguro que podrá entender un texto avanzado de Java fácilmente. 2 Aunque un editor de audio tan complejo como los que existen hoy día puede ser creado en Java, no es buena idea usar este lenguaje de programación para programas tan complejos ya que como veremos más adelante, Java es un lenguaje que debe ser interpretado por nuestro computador y esto lo hace un poco más lento para aplicaciones tan demandantes. 4 Personalmente tengo experiencia programando en php3, ActionScript 3.0 y JavaScript4 que aunque no son lenguajes para lograr lo que queremos en audio si me permiten tener la experiencia para explicarles de forma clara y tratar de evitarles todos los errores que cometí cuando empecé a programar. Yo sé lo difícil y tedioso que puede llegar a ser aprender un lenguaje de programación cuando ni siquiera entendemos bien qué es un lenguaje de programación, y esta es por lo general la realidad de los Ingenieros de sonido. Así que los ayudaré a entender sin necesidad de saber nada. No sólo pretendo que el lector pueda entender y divertirse en el proceso, pretendo crear conciencia sobre la necesidad de explotar otras formas de negocio que son tan necesarias hoy día en cualquier carrera. Durante la carrera, una gran mayoría de conocimientos adquiridos en materias como audio digital y audio analógico, entre otras, fueron vistos de manera teórica únicamente, sin poder entender una verdadera aplicación de los mismos. Más de 100 páginas de este proyecto de grado están enfocadas en aplicaciones reales del mundo de la programación, que permitirán entender de forma práctica muchos de estos conocimientos que a veces creemos que no tienen ningún fundamento o no sirven para nada. Al finalizar este texto, el lector tendrá la habilidad de entender el lenguaje Java de forma básica pero robusta, entendiendo la importancia de la programación orientada a objetos. También podrá entender de manera general las facilidades que nos brinda este lenguaje en el mundo del audio. De forma práctica, el lector podrá crear un metrónomo completo que le permitirá entender el uso de Java en esta aplicación de la vida real. 3 php es un lenguaje muy importante y popular en las páginas de internet. Aunque no permite trabajar con audio es casi siempre el elegido para trabajar con bases de datos. Páginas como facebook existen gracias a la programación en php. 4 JavaScript es un lenguaje que está presente en la gran mayoría de páginas de internet. No se puede confundir con Java ya que son lenguajes completamente diferentes. Gracias a este lenguaje apareció una forma de programación muy popular llamada AJAX que ha permitido crear páginas que parecen más un software que una página en sí. Gracias a AJAX podemos usar páginas como http://maps.google.com/ 5 A lo largo de este texto, evitaré el uso de tercera persona, típico de escritos formales, para acercarme al lector de una manera más personal, que permite desde el punto de vista pedagógico, entender más fácilmente temas que puedan llegar a ser complejos. Si bien varios de los programas que sugeriré para el desarrollo de Java podrían venir en un anexo digital a este proyecto de grado, es importante entender que nuevas versiones actualizadas y gratis pueden descargarse desde internet. Es por esta razón que en vez de agregar los programas que en cualquier momento quedarán obsoletos, escribiré las páginas desde las cuáles se pueden bajar todas las herramientas necesarias para seguir este escrito. Debido a que el metrónomo que se creará hacia el final del texto es de uso comercial privado, éste tampoco puede ser anexado al trabajo y es tomado únicamente como referencia de cómo programar una aplicación en el mundo productivo. Si por una razón Java sobresale entre tantos lenguajes de programación es por todo su poder de control y por su slogan "write once, run everywhere", esto significa que escribimos el código una vez y el mismo resultado nos funciona en MAC, PC, dispositivos móviles e internet. Esta flexibilidad y robustez no se encuentra en ningún otro lenguaje de programación famoso. Estamos a punto de aprender un lenguaje tan poderoso que es usado para crear los menús y permitir la reproducción de Blu-Ray, se ha usado en el Robot que la NASA envió a Marte y también es usado en el famoso Kindle. Usaremos Java 1.6 SE. Quiero recomendarle a todo el que vaya a leer de aquí en adelante, que tenga en cuanto sea posible un computador y ya empezaremos a hablar de cómo instalar y qué necesitamos para empezar a programar. La gran mayoría si no todos los programas que usamos aquí son gratis para descargar, así que empecemos a programar en Java desde la perspectiva del audio. 6 Antes de empezar Para todos los que nunca han programado, debo pedirles que preparen su cuerpo a nuevas formas de pensamiento. Tal vez la forma de pensar más parecida a la programación es la que usamos en las matemáticas, pero el proceso de aprendizaje se parece mucho más a aprender un nuevo idioma ya que implica asimilar nuevas palabras y entender una nueva sintaxis, esto es la forma en que se combinan esas nuevas palabras creando significados específicos. Una vez entendemos el idioma debemos hablar y oír mucho en el mismo para poder manejarlo. Lo mismo ocurre con los lenguajes de programación. A los programadores les encanta usar siglas y acrónimos para nombrar cualquier cosa que inventan, así que estén listos para aprenderlos todos. Empecemos por dos muy importantes: JRE y JDK. El primero significa 'Java Runtime Environment' que es el software necesario para correr aplicaciones creadas en Java. El segundo significa 'Java Development Kit' que es el software que nos permite desarrollar y crear aplicaciones que luego podremos ver usando el JRE. Entonces para crear aplicaciones necesitamos el JDK y para verlas necesitamos el JRE. Java viene en dos presentaciones: Java SE 'Standard Edition' y Java EE 'Enterprise Edition'. En la edición EE hay funciones extra con respecto a la edición SE que por ahora no necesitamos y se salen de los límites de este escrito. Estamos a punto de empezar a descargar el software necesario. Antes debo aclarar que hay varios caminos que podemos tomar para desarrollar aplicaciones en Java. Primero vamos a ver un camino largo y tedioso y después vamos a ver el camino corto y agradable. ¿Por qué ver el camino largo y tedioso? Porque esta es la forma básica de programar en Java y debemos conocerla para poder entender procesos que están ocurriendo a escondidas en el camino corto y agradable. Sin mostrarles el camino largo no podríamos entender las bases que gobiernan la programación. Además en el camino corto usaremos un software muy particular y 7 no me parece buena idea que dependan de éste para programar en Java. Si por ejemplo un día este programa que nos permite ir por el camino corto dejara de existir, aunque es poco probable, igual tenemos los conocimientos para hacerlo de la forma básica. Con esto claro, aprendamos el camino largo y aburrido para empezar a divertirnos con el camino corto más rápidamente. Lo primero es ir a http://www.java.com/es/ y bajar el JRE, esto es todo lo que necesitarás para correr aplicaciones creadas en Java. Si por ejemplo has terminado una aplicación y se la quieres mostrar a alguien, esa persona lo único que necesitará es el JRE y es probable que ya lo tenga instalado. Para nosotros los que vamos a programar necesitamos también el JDK que trae incluido el JRE al descargarlo. Para descargarlo vamos a http://www.oracle.com/technetwork/java/javase/downloads/index.html y una vez allí buscamos el JDK para Java. Para la fecha actual de este escrito, el JDK está en su versión 6 en la actualización 19. Es probable que para cuando tú lo bajes haya una versión más nueva y eso no es ningún problema porque afortunadamente Java siempre es compatible con sus versiones anteriores. La descarga e instalación del JDK y el JRE es bastante sencilla. Debes tener en cuenta en qué parte de tu computador quedan guardados. Si encuentras problemas en el camino busca los manuales de instalación en las mismas páginas que te mencioné antes. Además del JDK necesitamos un editor de texto. Programas como Microsoft Word no sirven porque cada vez que escribimos, el software está guardando información adicional y cierto tipo de meta datos que no vemos y que no van a permitir que nuestro programa funcione correctamente o simplemente no funcione, porque además le agregan a todo lo que escribimos una extensión propia del editor, en el caso de Word agrega '.doc' o '.docx' y para Java necesitamos crear archivos con extensiones '.java' en primer lugar. Podemos usar programas como NotePad, aunque existen cientos de editores especializados para Java que corrigen 8 nuestros errores gramaticales y tienen otras herramientas que nos pueden ser muy útiles, pero veremos sobre estos más adelante. Veamos como es el proceso de creación de una aplicación cualquiera usando el JDK y NotePad. Con nuestro JDK instalado vamos a usar NotePad para escribir nuestro primer código Java. Normalmente, cuando estamos desarrollando una aplicación, debemos ir probando partes del código para saber si funciona y para esto necesitamos compilar. Para saber qué es compilar debemos tener claro que el código que escribimos está hecho para que nosotros los humanos lo entendamos más fácilmente, pero este tipo de lenguaje es muy abstracto para las máquinas y aunque lo pueden entender, deben primero ser traducidos a lenguajes que las máquinas puedan descifrar más rápido y así nuestra aplicación corra rápidamente. Si nuestro computador usara nuestro código fuente5 cada vez que corre el programa, el resultado serían aplicaciones lentas. Entonces al compilar, lo que está ocurriendo es que nuestro código se está traduciendo a otro código que nosotros no entendemos pero que la máquina si entiende mucho más fácilmente. El lenguaje que mejor entiende cada computadora es el código máquina, éste es el ideal en cuanto a velocidad para los programas. El problema con el código máquina es que depende del sistema operativo y del procesador, esto quiere decir que necesitamos diferentes códigos máquinas para diferentes lugares donde queramos probar nuestras aplicaciones. Cuando compilamos en Java no obtenemos un código máquina como si ocurre con otros lenguajes famosos. Lo que obtenemos al compilar en Java es un código llamado bytecode. Este código es muy cercano al código máquina y esto es lo que le permite ser tan rápido. Lo bueno que tiene el bytecode es que para todos los sistemas operativos es el mismo. De cualquier forma Java necesita alguien que traduzca el bytecode a código máquina y para esto usa la máquina virtual JVM que significa 'Java Virtual Machine'. JVM es un software que se instala con el JRE y que corre 5 Código fuente es el código que nosotros mismos escribimos en un lenguaje de programación particular, en este caso Java. Por cuestiones de derechos de autor por lo general no queremos que este código lo vea nadie. 9 automáticamente cada vez que abrimos una aplicación Java. Sin JVM no podrían los sistemas interpretar el bytecode, y sin bytecode no podría Java tener la portabilidad que tiene y no podría tener su lema "Write once, run everywhere". Resumiendo un poco, nosotros creamos un código Java que es traducido a bytecode cuando compilamos. El bytecode es interpretado por la máquina virtual Java o JVM que viene con el JRE. Ya entendiendo qué es compilar, podemos seguir viendo cómo es el proceso general al crear una aplicación. Suponiendo que ya tenemos nuestro código en Java listo para probarlo, para compilar debemos usar la línea de comandos en Windows o la Terminal en Mac. Yo estoy trabajando en Windows 7, 64 bits en inglés y encuentro la línea de comandos en Start > All Programs > Accessories > Command Prompt. Dependiendo de tu sistema operativo esto puede cambiar pero no debe ser difícil, si no encuentras tu línea de comandos simplemente busca en Google cómo abrirla para tu sistema operativo. Para Mac tengo entendido que se encuentra en la carpeta Utilities > Applications > Terminal. Les voy a dar un código en Java que en realidad no hace mucho pero va a ser muy útil para que sigan los pasos conmigo y así aprendan cómo se compila y cómo se corre el programa que hemos creado en Java. Por ahora no importa que no entiendan nada del siguiente código, más adelante veremos lo que significa y qué hace exactamente cada parte. Con el JDK ya instalado, escribe el siguiente código en NotePad respetando las mayúsculas y minúsculas, ten cuidado también con el tipo de paréntesis que usas ya que deben ser exactamente los mismos que usamos aquí. Debemos diferenciar entre paréntesis (), corchetes [ ] y llaves { }. La cantidad de espacio en blanco no es significante si es por fuera de las comillas. Para Java es igual un espacio que cinco espacios si estamos fuera de unas comillas. 10 public class MiPrimerPrograma { public static void main (String[ ] args) { System.out.print("No soy un programa de audio pero pronto lo seré."); } } Usa un procesador de texto básico como NotePad en el que puedas escribir la extensión .java y nombra este archivo MiPrimerPrograma.java y debes estar muy pendiente del sitio dónde lo guardas. Java diferencia entre mayúsculas y minúsculas así que debes escribir el nombre exactamente como te lo indico. Para compilar este código necesitamos saber dos ubicaciones: 1. Necesitas saber dónde quedó instalado el JDK de Java y la carpeta llamada 'bin'. Esto depende de lo que hayas seleccionado durante la instalación y los números dependen de la versión que hayas instalado en tu sistema. En mi equipo está en: C:Program Files\Java\jdk.1.6.0_19\bin 2. Necesitas saber dónde está guardado el archivo MiPrimerPrograma.java. En mi computador está en: D:ESTUDIO\PROYECTO_DE_GRADO\programas\intro\MiPrimerPrograma.java Recomiendo que los nombres de las carpetas no tengan espacios, excepto los que no podemos cambiar como 'Program Files'. Ahora vamos a la línea de comandos de nuestro sistema operativo. Ya abierta, en mi caso, al abrirla veo que está por defecto en la ubicación C:\users\Juan como muestra la imagen 1. 11 1. Imagen inicial de mi Línea de comandos. Debemos cambiar esta ubicación por la de la carpeta 'bin' del JDK. Para devolvernos hasta C: desde cualquier ubicación dentro de C: escribimos cd\ en la línea de comandos y hacemos enter para quedar en la imagen 2. 2. Nos ubicamos en C: Ahora para movernos hasta nuestra carpeta deseada escribimos cd más la ruta de la carpeta sin escribir C: porque ya estamos allí, en mi caso sería así: cd Program Files\Java\jdk1.6.0_19\bin El comando cd significa 'change directory'. Al hacer enter ya debemos estar en nuestra carpeta bin. Como muestra la imagen 3. 12 3. Ubicados en la carpeta bin. Desde allí vamos a escribir javac que es la señal que le enviamos a Java para que compile nuestro código, seguida de la ruta donde está el código fuente, en mi caso sería así: javac D:ESTUDIO\PROYECTO_DE_GRADO\programas\intro\MiPrimerPrograma.java Al hacer enter, si escribimos todo correctamente, volvemos a ver la ruta de nuestra carpeta 'bin'. Sin ningún mensaje. 4. Con nuestro archivo ya compilado. Hasta aquí nos vamos dando cuenta que es un proceso tedioso y debe haber formas más fáciles de hacerlo, pero quiero tocar puntos importantes con este 13 proceso así que sigamos. Con todos los pasos anteriores ya debemos tener nuestro programa compilado. Para saber cuál es el resultado, en nuestra carpeta donde pusimos nuestro código fuente, ahora debemos ver un archivo que se llama MiPrimerPrograma.class. Este es el resultado después de haber compilado, este archivo está en bytecode. Pero ahora para correr nuestro primer programa debemos volver a la línea de comandos y movernos hasta la carpeta donde tenemos nuestro archivo resultante de la compilación. Como el archivo resultante está en otro disco duro en mi caso, primero debemos escribir d: y luego hacer enter en nuestra línea de comandos. Siempre que queramos cambiar la raíz del directorio simplemente escribimos su letra seguida de dos puntos y hacemos enter sin importar donde estemos. Ya en este punto estamos parados en D:, lo que tenemos que hacer es movernos hasta la carpeta de nuestro archivo MiPrimerPrograma.class, que en mi caso sería así: cd ESTUDIO\PROYECTO_DE_GRADO\programas\intro 5. Ubicados en la carpeta del archivo compilado. Una vez en la dirección correcta simplemente escribimos lo siguiente para ver qué hace nuestro programa: java MiPrimerPrograma 14 No le agregamos la extensión ni nada y ahora debemos ver lo siguiente en nuestra línea de comandos: 6. Este es el resultado de nuestro primer programa. El programa dice: No soy un programa de audio pero pronto lo seré Seguido de la ruta donde está nuestro programa como muestra la imagen 6. Hasta aquí nos queda claro que nuestro primer programa no hace mucho, simplemente es un programa que dice algo en la línea de comandos, pero nos acaba de enseñar muchas cosas. Primero aprendimos que este proceso es muy aburridor y ni yo mismo quiero volver a hacerlo, pero también aprendimos que compilar es el proceso que nos convierte nuestro código en un archivo .java a bytecode en un archivo .class que puede ser usado por la máquina virtual de Java. También aprendimos que podemos usar la línea de comandos para compilar programas desde la carpeta 'bin' usando la palabra clave javac seguida de la ubicación del archivo que queremos compilar. También podemos ejecutar el código ya compilado desde la ubicación de nuestro archivo .class con la palabra clave java seguida del nombre de nuestro programa sin extensión. 15 Es bueno saber que la línea de comandos sirve para algo ¿no? En la vida real cuando estamos creando aplicaciones de verdad y que son mucho más útiles que esta primera aplicación, debemos estar compilando y probando muy seguido para averiguar si tenemos errores en nuestro código. ¿No sería un sueño que tuviéramos un ayudante que compilara y corriera el código por nosotros? Debido a que este proceso que hemos hecho hasta aquí nadie se lo aguanta, aparecieron ciertos programas llamados IDE que significan 'Integrated Development Environment'. Estos programas son software que nos permiten escribir nuestro código, avisarnos de errores mientras escribimos, compilar nuestro código, comprobar más errores y correr la aplicación. Todo en un solo programa. Este es el camino fácil y agradable y es el que usaremos de aquí en adelante. Uno de los IDE más famosos para Java, que además es gratis, es NetBeans. Lo podemos descargar para los sistemas operativos Mac, Linux, Windows y Solaris desde http://www.netbeans.org. Si bien yo recomiendo y uso este editor para crear aplicaciones en Java hay muchos otros gratis y otros que podemos obtener pagando y seguramente la mayoría son muy buenos. Lo mejor de todo es que al descargar NetBeans no tenemos que descargar ni siquiera el JDK ya que viene incorporado con el programa. Este software es muy completo y no pretendo que aprendan a manejarlo todo aquí, en la página antes mencionada podemos encontrar buenos tutoriales sobre cómo usarlo. Sin embargo les enseñaré cuestiones básicas en el siguiente capítulo, de tal forma que cada vez que tengamos un código completo sepamos que para probarlo, primero debemos compilarlo y luego ejecutar el programa. Compilar y ejecutar en NetBeans se hace con un solo clic en un botón. En este punto quiero hacer un rápido resumen de lo que no deben olvidar para que podamos empezar a ver NetBeans y el lenguaje en sí. 16 Cuando vamos a crear un programa en Java necesitamos un IDE 'Integrated Development Environment' que es un software que nos permite crear mucho más fácilmente nuestras aplicaciones y probarlas de manera agradable. También necesitamos un JDK 'Java Development Kit' que nos permite desarrollar aplicaciones y que contiene un JRE 'Java Runtime Environment' que nos permite ver las aplicaciones y que contiene un JVM 'Java Virtual Machine' que sirve para ejecutar el bytecode que es un lenguaje muy cercano al código máquina. Afortunadamente el JDK viene con nuestro IDE NetBeans. Hay diferentes IDE dependiendo del lenguaje que vamos a usar, en este caso NetBeans es especializado en Java pero también sirve para C, Ruby, JavaFX y php que son otros lenguajes famosos. Si le queremos mostrar a nuestros amigos y familiares nuestras creaciones en Java, ellos solo necesitan un JRE. Más adelante veremos cómo hacer para entregarles un archivo al que puedan hacer simplemente doble clic para abrir, mientras aprendemos eso, ellos tendrían que usar la línea de comandos para poder abrirlo. En todo caso el archivo que podríamos entregar mientras tanto es el .class y no el .java. Recordemos que el archivo con extensión .java es nuestro código fuente y no queremos que nadie lo vea. Aunque todavía no hemos visto nada de Java en sí, ya entendemos que los lenguajes de programación se hicieron pensando en que los seres humanos pudieran crear programas partiendo de códigos que pudieran entenderse. Estos lenguajes de programación deben ser traducidos para que las máquinas los entiendan más fácilmente. El proceso de traducir estos lenguajes se llama compilar. Para compilar en Java necesitamos un JDK. Al compilar un archivo .java obtenemos un archivo .class que está en bytecode. Este código es leído por la máquina virtual Java o JVM que viene cuando tenemos un JRE. Para facilitarnos la vida usaremos un IDE llamado NetBeans que nos permite escribir y compilar nuestros programas de manera sencilla sin tener que usar la línea de comandos. 17 NetBeans IDE Como todos queremos aprender Java rápido y el tema es largo, no pretendo profundizar sobre NetBeans. No hablaré de la historia ni de cuestiones particulares sobre este software. Lo que me interesa en este capítulo es que puedan usar NetBeans para empezar a explorar Java, después por su cuenta pueden aprender más al respecto. Como lo mencioné en el capítulo anterior, NetBeans es un IDE, siglas que significan 'Integrated Development Environment' y esto quiere decir que es un entorno en el que podemos desarrollar programas más fácilmente. En el capítulo anterior vimos lo tedioso que puede ser crear programas en Java cuando no tenemos un IDE. Gracias a este software podremos encontrar errores en nuestro código rápidamente y podremos estar viendo y oyendo los resultados que producen nuestros códigos muy fácilmente. Descargar NetBeans es muy fácil. Lo podemos hacer desde la siguiente dirección: http://netbeans.org/downloads/index.html donde podremos escoger la versión que queremos descargar. No puedo asegurar cómo se verá la página para el momento que ustedes la visiten, pero actualmente me deja escoger si quiero que me sirva para JavaFX, Java EE, php, C, C++ y hasta trae servidores para descargar. Como vamos a crear programas usando Java SE, esta sería la opción que debemos escoger, aunque la versión completa también funciona, solo que trae muchas más tecnologías para desarrollar. En la página puedo ver que hay enlaces para bajar NetBeans con el JDK directamente o si lo prefiero puedo bajarlos aparte. Lo importante es que al final del proceso tengamos el JDK y NetBeans. Cada proceso de instalación puede variar dependiendo del sistema operativo así que en este punto deben referirse al manual de instalación que pueden encontrar en la página de NetBeans. Normalmente es un proceso que debe ser muy sencillo. Yo usaré la versión 6.9.1 que funciona solamente para desarrollar aplicaciones en Java SE. 18 Previamente ya tenía instalado el JDK y ustedes también si siguieron conmigo los pasos del capítulo anterior. Cuando instalé NetBeans, el programa me preguntó en qué carpeta estaba mi JDK que encontró automáticamente. Vamos a crear el mismo proyecto que hicimos por el camino difícil con la línea de comandos pero ahora lo haremos en NetBeans. Los pasos son muy sencillos y van a ser los mismos cada vez que hagamos un nuevo proyecto. 1. Abrir NetBeans. Lo primero que veremos es la presentación del programa que tiene unos enlaces a la página donde podemos aprender más sobre el software y otra información adicional. 2. Empezar un nuevo proyecto. Mi NetBeans está en Inglés así que primero buscamos en el menú y hacemos clic en File y luego New Project. 3. En las categorías escogemos Java, en proyectos escogemos Java Applications y luego hacemos clic en Next. 19 4. Nombramos el proyecto MiSegundoPrograma y seleccionamos la carpeta en la que queremos nuestro proyecto. Seleccionamos la caja que dice 'Create Main Class' y la nombramos MiSegundoPrograma también. Seleccionamos la caja 'Set as Main Project'. Por último hacemos clic en Finish. Aunque crear el proyecto nos toma más tiempo ahora, las ventajas las veremos al compilar y ejecutar nuestro programa. No olvidemos que crear un nuevo proyecto solo lo haremos cada vez que queramos crear una nueva aplicación y esto no ocurre muy seguido, en cambio compilar y correr aplicaciones lo hacemos miles de veces mientras probamos nuestro código. 5. Escribimos el código. Vemos un código que se generó automáticamente pero por ahora lo vamos a borrar todo y lo vamos a reemplazar por el siguiente: 20 public class MiSegundoPrograma { public static void main (String[ ] args) { System.out.print("No soy un programa de audio pero pronto lo seré."); } } 6. Compilamos y corremos nuestro código con el botón que tiene una flecha verde como vemos en la siguiente imagen: 7. Vemos el resultado en la ventana Output Aquí vemos el resultado que antes teníamos en la línea de comandos. Esto quiere decir que la ventana Output hace las veces de una línea de comandos. Como podemos ver necesitamos 7 pasos para crear un nuevo proyecto en Java usando NetBeans. Este proyecto ya está guardado y al abrir NetBeans nuevamente ya lo tendremos a nuestra disposición. Lo más interesante es que si tenemos errores en nuestro código, NetBeans nos dirá señalándonos la línea del código con problemas. El paso 6 es el que más haremos cada vez que modifiquemos algo en nuestro código y ahora es muy fácil de realizar. 21 En esta imagen vemos las tres ventanas principales de NetBeans: En la ventana 1, debemos asegurarnos que estemos en la pestaña Projects o en Files, es donde encontraremos algunos de nuestros proyectos ya abiertos con el programa. Es poco lo que haremos en esta ventana pero nos sirve para ver la organización interna de nuestros proyectos y se vuelve muy útil cuando tenemos muchos archivos en proyectos grandes. En la ventana 2 es donde escribimos todo nuestro código Java. Debemos tener cuidado porque dependiendo de la pestaña superior podemos estar en diferentes archivos .java y podríamos terminar modificando uno no deseado. En la ventana 2 vemos a la izquierda de nuestro código unos números que son los encargados de numerar las líneas. Esto es muy importante ya que cuando tenemos un error en el código, NetBeans mostrará una alerta o un símbolo rojo sobre la línea con problemas. Por último en la ventana 3 veremos los resultados. En realidad esta es una ventana de prueba porque las aplicaciones queremos verlas con interfaces gráficas agradables para el usuario por lo que la ventana tres termina siendo útil para probar ciertos resultados antes de mostrarlos al usuario en su interfaz gráfica. 22 Muchas veces nos puede pasar que hacemos un mal movimiento dentro del programa y se nos desaparece alguna de nuestras ventanas principales. Si se nos cerró un archivo en el que estábamos editando lo buscamos nuevamente en la ventana Projects o en Files. Si la ventana Projects, Files o Output se nos ha cerrado, podemos volver a abrirlas desde el menú en la barra superior, en el ítem Window. Como vimos en el capítulo anterior, después de compilar terminamos con un archivo .class que no es fácil de abrir ya que necesitamos usar la línea de comandos que no es tan agradable. La solución a esto es crear un archivo JAR que significa Java ARchive. Este es un tipo de archivo al que solo tenemos que darle doble clic para poder abrirlo y así nuestro programa se ejecutará. Un archivo .jar se comporta muy parecido a un .rar o un .zip ya que su función es guardar varios archivos dentro de un solo .jar. Aunque podemos crear archivos JAR desde la línea de comandos, les voy a enseñar cómo hacerlo directamente desde NetBeans que es mucho más fácil y así no tenemos que entrar a entender procesos que van a dañar nuestra curva de aprendizaje. Para crear un archivo .jar de nuestra aplicación simplemente hacemos clic en el botón que se llama 'Clean and Build Main Project'. que es el que muestra la siguiente imagen: Al presionarlo podemos ver en nuestra ventana Files una carpeta llamada dist que contiene un archivo llamado MiSegundoPrograma.jar. Si lo buscamos en nuestro computador y hacemos doble clic sobre él, nada va a ocurrir. No pasa nada porque recordemos que nuestro programa solamente decía algo en la línea de comandos o en el Output de NetBeans y recordemos que tanto la línea de comandos como la ventana Output son de prueba, por lo que el usuario final no verá lo que sale allí. Para que el usuario final vea algo en pantalla tenemos que crear interfaces gráficas y más adelante veremos cómo hacerlas. 23 Anatomía de Java A partir de este punto empezaremos a aprender el lenguaje en sí. Entiendo que los procesos anteriores pueden llevar a muchas preguntas y pueden tener asuntos no tan agradables, pero una vez hecho todo lo anterior estamos con nuestro computador listo para crear los primeros códigos en Java los cuales si vamos a entender cómo funcionan. Al comienzo veremos códigos completos que puedes ir y copiar exactamente iguales en NetBeans para probarlos y entenderlos al modificarlos a tu gusto. Cuando estemos un poco más adelantados ya no es práctico que yo escriba todo el código completo porque terminaríamos con muchas páginas de códigos, así que puedo empezar a escribir partes de códigos que por sí solos no funcionan, pero para entonces ya tendrás los conocimientos necesarios para descifrar qué es lo que hace falta para que funcionen al compilar. Existen ciertos errores en programación que NetBeans puede detectar antes de compilar, pero hay otros errores que no saldrán hasta el momento de compilar o incluso aparecerán mientras corre nuestra aplicación. Debemos estar muy pendientes de la lógica de nuestros códigos, de palabras mal escritas, tener cuidado con mayúsculas y minúsculas y no debemos preocuparnos ni desesperarnos porque los errores en el código son parte de la vida diaria de hasta el mejor programador del mundo. Empecemos por entender el código más básico que se puede hacer en Java: public class NombreDeLaClase{ public static void main (String[ ] args) { System.out.print("Esto saldrá en la ventana Output"); } } 24 En este punto ya hemos visto un par de códigos muy parecidos a este. Este código tiene exactamente la misma estructura y forma que MiPrimerPrograma.java y MiSegundoPrograma.java. Además producen el mismo resultado que es escribir algo en la ventana de salida o Output. Aunque este programa no hace nada excepcional, es la estructura más básica que se puede crear en Java para que funcione. Vamos a descifrar qué es lo que está ocurriendo línea por línea. public class NombreDeLaClase{ La anterior es nuestra primera línea. La primera palabra que vemos es public y hace parte de algo llamado modificador de acceso. Por ahora no voy a complicar más la situación, con que sepamos que public es un modificador de acceso es suficiente, más adelante profundizaremos en el asunto. Toda aplicación en Java debe estar contenida en una clase, como las llamamos en español, que en java y en inglés se escribe class. Una clase es un contenedor para nuestro código. En nuestros dos primeros programas y en este, tenemos una sola clase que hemos nombrado con la palabra que escribimos después de MiPrimerPrograma, MiSegundoPrograma o NombreDeLaClase. class ya sea En realidad podemos poner el nombre que queramos pero dicho nombre no puede tener espacios, no puede tener signos de puntuación ni caracteres raros y por convención deben empezar siempre con mayúscula. Para facilitar la lectura de estos nombres de clases, que por lo general están creados a partir de varias palabras para ser más descriptivos, debemos usar algo llamado CamelCase. Esto es cuando escribimos palabras unidas, pero para facilitar la lectura ponemos en mayúscula la primera letra de cada nueva palabra. EsMasFácilLeerEsto que tenerqueleeresto. Después del nombre de nuestra clase viene una llave de apertura { que funciona para determinar que de ahí en adelante se escribirá el código perteneciente a la clase hasta que encontremos su respectiva llave de cierre } que es la que aparece 25 en la última línea o línea 5. Estas llaves determinan el comienzo y final de una porción de código que llamaremos bloque. Cada vez que tengamos código dentro de unas llaves tenemos un bloque de código. Toda llave, corchete, paréntesis y paréntesis angular que se abra {, [, ( y < debe cerrarse con su correspondiente }, ], ) y > sino el código no compilará. En definitiva las clases se usan para encerrar porciones de códigos por razones que veremos claramente más adelante. Por ahora imaginemos que estamos diseñando un reproductor de audio. Una buena idea sería usar una clase para contener todo el código que permite funcionar al reproductor, este sería el código encargado de cargar las canciones, encargado de hacer pausar la canción cuando el usuario lo desea, subir y bajar el volumen, etc. Otra clase podría tener todo el código correspondiente a la parte visual del reproductor, lo que se denomina interfaz gráfica, como los botones, el contenedor que muestra la carátula del álbum, los colores de fondo, etc. Un programa en Java puede tener varias clases y éstas pueden comunicarse entre sí como lo veremos más adelante. En nuestro ejemplo del reproductor de audio es necesario que ambas clases se comuniquen entre sí, ya que la parte visual seguramente dependerá de la canción que esté sonando. Por esta razón es que existen los modificadores de acceso como public. Imaginemos que tenemos una clase llena de código al cual no queremos que ninguna otra clase pueda acceder por seguridad, en ese caso usamos los modificadores de acceso para proteger nuestra clase del resto escribiendo por ejemplo private en vez de public. Ahora pasemos a la segunda línea de código: public static void main (String[ ] args) { Si miramos en el código original esta línea, veremos que está tabulada. Como ya se dijo antes, la cantidad de espacio en blanco no determina que esté bien o mal escrito el código, de hecho podríamos escribir todo el código en una misma línea, 26 pero hacerlo así no facilita su lectura. Al tabular estamos dando a entender visualmente que la segunda línea está contenida en la clase. Es una ayuda visual. Como podemos ver esta segunda línea también empieza con un modificador de acceso public. En esta segunda línea estamos creando un método. Tanto los métodos como las clases tienen modificadores de acceso para proteger su información. Los métodos van dentro de las clases y son como clases en cuanto que guardan porciones de información más específica en bloques. Volvamos al ejemplo del reproductor de audio. Si estamos escribiendo el código para un reproductor de audio, es probable que necesitemos separar pedazos de código dentro de su clase. Supongamos que estamos haciendo nuestra clase que se encarga de la funcionalidad del reproductor, es probable que queramos separar el código que se encarga de pausar la canción, del código que hace subir el volumen, ya que no queremos que cuando el usuario vaya a hacer pausa se suba el volumen, ¡sería un DESASTRE! Entonces por lo anterior es que existen los métodos y eso es lo que se está creando en esta segunda línea de código. Después de public vemos las palabras static void main (String[ ] args) { de las cuales les puedo decir que ya mismo no nos interesa saber exactamente qué es todo eso, más bien aprendamos que todo eso crea un método muy importante que es el método llamado main como indica la cuarta palabra en esta segunda línea. Toda aplicación en Java debe tener un método main que es el que va a correr automáticamente cada vez que ejecutemos nuestro programa. Pensemos en el caso del reproductor de audio, no queremos que todos los métodos se ejecuten cuando iniciamos el reproductor porque sería caótico. En cambio solo un método corre automáticamente cuando empieza nuestra aplicación y ese es el método main. Una aplicación puede tener muchas clases y muchos métodos pero sólo un método main. Una clase puede no tener un método main. El resto de métodos se pueden disparar desde este principal o cuando ocurre un evento que es cuando el usuario hace clic sobre un botón o algo parecido. 27 Aunque más adelante veremos los métodos detenidamente, veamos que en la segunda línea aparecen unos paréntesis, y así como aprendimos antes aparece el paréntesis que abre ( y luego cierra ). Lo mismo ocurre dentro de los paréntesis con unos corchetes [ ] que más adelante veremos lo que significan. Al final de la línea dos, vemos una llave de apertura que indica que a partir de ahí empieza todo el código que hace parte de este método main y que se acaba con la llave de cierre } en la línea 4. Por último tenemos la línea tres con el siguiente código: System.out.print("Esto saldrá en la ventana Output"); Sin profundizar mucho, este código es el encargado de escribir algo en la ventana Output o en la línea de comandos. ¿Qué escribe? Lo que sea que pongamos dentro de las comillas. Como ya se mencionó antes, escribir en la ventana de salida se usa para cuestiones de pruebas y por agilidad. Nada de lo que escribamos en este código podrá ser visto fuera de la línea de comandos, esto quiere decir que una persona que hace doble clic sobre un archivo JAR no puede ver esta información a menos que ejecute el JAR desde la línea de comandos, cosa que es poco probable. Este código lo terminaremos aprendiendo así no queramos de tanto que se usa cuando creamos una aplicación. Mientras estamos en el proceso de desarrollo, casi todos los programadores usan en varios puntos este código para saber qué está ocurriendo con x porción de código. Por más que estos resultados no los vea el usuario final, siempre se borran y solo se usan para probar nuestro código como ya veremos más adelante. En muchas de las aplicaciones usaremos la ventana de salida para aprender a usar Java, así que veremos mucho este código. Repasando un poco veamos en colores el código para entenderlo de forma general un poco mejor y le vamos a agregar una línea de código extra: 28 public class NombreDeLaClase{ public static void main (String[ ] args) { System.out.print("Esto saldrá en la ventana Output"); System.out.println("Esto también saldrá en la ventana Output"); } } La línea 1 y 5 están en rojo por ser el comienzo y el final de la clase que nombramos NombreDeLaClase. El nombre de toda clase que contiene al método main debe ser igual al nombre del archivo .java en el computador que lo contiene. En este caso el código debe ir en un archivo llamado NombreDeLaClase.java. Veamos que cada vez que estamos escribiendo dentro de llaves usamos tabulación para ordenar visualmente los contenidos. Dentro de la clase encontramos el método main que está en azul. Dentro de main tenemos en verde el código que se ejecuta automáticamente cuando se carga la aplicación. Este código es el encargado de imprimir algo en la ventana Output y le hemos agregado una línea muy parecida debajo que imprime otra oración. Cada sentencia en el código verde termina en punto y coma. Todo código que haga algo específico por pequeño que sea se le pone punto y coma para separarlo del resto, esto es una forma de decir fin de la sentencia. Cuando veamos más código más complejo en Java empezarás a entender qué lleva punto y coma y qué no. Por ahora piensa que toda sentencia que escriba en la ventana Output termina con punto y coma. Mira que el código verde tiene otra diferencia y es que uno dice System.out.print y el otro dice System.out.println. El primero imprime en la misma línea y el segundo hace un salto de línea, como cuando hacemos enter en Microsoft Word. Escribe este código en NetBeans y juega con las impresiones en Output agregando los que quieras, para que compruebes por ti mismo cómo funciona. Como conclusión Java usa clases, dentro de las clases van métodos y dentro de los métodos van sentencias que terminan en punto y coma. 29 Variables En el capítulo anterior aprendimos que para empezar a crear una aplicación en Java primero creamos una clase y la nombramos empezando en mayúscula y usando CamelCase. Dentro de esa clase va un método main que se ejecuta automáticamente cuando la aplicación carga. Dentro de este método principal pondremos nuestro código para empezar la aplicación. Más adelante veremos cómo crear varios métodos dentro de una misma clase. También veremos cómo crear más clases que podemos usar en una misma aplicación. Ya sabiendo la anatomía básica que usa Java, debemos preocuparnos un poco más por el código que podemos escribir dentro de los métodos. Aprender algunas cuestiones básicas de Java y seguir aprendiendo más sobre la anatomía nos tomará algunos capítulos, pero con un poco de paciencia vamos a poder aplicar todos estos conocimientos para hacer casi cualquier aplicación de audio que se nos ocurra. Sin embargo enfocaré en cuanto más pueda los ejemplos a códigos que pudieran darse en aplicaciones de audio. Como dije antes, usar un lenguaje de programación requiere un pensamiento matemático. En ecuaciones de matemáticas existen las variables. Recordemos que una variable es simplemente un contenedor que puede almacenar múltiples valores. Por lo general en matemáticas se usan las variables para nombrar una porción de la ecuación que desconocemos en dicho punto. De la misma forma en Java existen las variables pero tienen un uso mucho más allá de los números. Existen varios tipos de variables en Java. Pensemos que no es saludable para nuestro programa que una variable que está pensada para contener solo números de pronto se llenara con texto que no tiene nada que ver con números. Sería tan problemático como tener una ecuación matemática en la que de pronto apareciera una palabra. Por lo anterior, en Java debemos especificar el tipo de variable que estamos creando. Existen variables solo para números enteros, otras para 30 almacenar texto entre comillas que los lenguajes denominan cadenas o String, y otros tipos de variables que veremos más adelante. Imaginemos que estamos creando un reproductor de música en Java, en algún punto sería buena idea crear una variable que cargara el nombre de la canción que está sonando. Veamos el siguiente código: public class Canciones{ public static void main (String[ ] args) { String cancion; cancion = "Airplane"; System.out.println(cancion); cancion = "The show must go on"; System.out.println(cancion); cancion = "Billionaire"; } } No podemos olvidar que este código debe ir en un archivo que debe llamarse igual a la clase que contiene el método main. Escribe el código anterior en NetBeans, crea un proyecto llamado 'Canciones' y en el campo que dice 'Create Main Class' escribe 'Canciones' para que NetBeans te cree un archivo llamado Canciones.java dentro del proyecto Canciones. NetBeans crea por nosotros la estructura básica que es la clase y el método main. Además vemos en gris otro tipo de código que en realidad son comentarios pero no es código. En el siguiente capítulo veremos qué son los comentarios en el código y para qué sirven, por ahora puedes borrar todo y escribir el código anterior. Cabe notar que pudimos nombrar el proyecto cualquier otra cosa, por ejemplo 'variables', y en 'Create Main Class' si debemos escribir 'Canciones' para que nos cree el archivo correcto. Las dos primeras líneas de código ya deben ser familiares para nosotros, aunque hay detalles de la creación del método que veremos más adelante. Todo método 31 main empieza como empieza nuestra segunda línea de código. En la tercera línea de código tenemos lo siguiente: String cancion; Esta tercera línea es la forma como inicializamos una variable. Toda variable se inicializa declarando el tipo y el nombre. En este caso estamos creando una variable de tipo String y con nombre 'cancion', sin tilde. Cada vez que inicialicemos o hagamos algo con una variable tenemos una sentencia, esto quiere decir que debe terminar con punto y coma. El tipo String es el necesario para indicar que una variable va a contener texto. Siempre que vamos a crear una variable, lo primero que escribimos es el tipo de contenido que va dentro de dicha variable. Para la variable que contiene el nombre de las canciones de nuestro reproductor, necesitamos que sea de tipo String. Incluso si tenemos una canción que se llama "4'33", de todas formas podemos poner números dentro de la cadena o String ya que estos son tratados como texto. Después del tipo, escribimos el nombre que le vamos a dar a nuestra variable. Este nombre debe usar CamelCase pero se diferencia del nombre de una clase porque empieza en minúscula. El nombre de la variable no puede empezar con números aunque puede contenerlos, no puede tener espacios y no se deben usar signos raros ni tildes. Esto es todo lo que necesitamos para crear una variable así que como ya terminamos ponemos punto y coma. Hasta aquí hemos creado una variable pero no le hemos asignado un contenido. En la siguiente línea tenemos: cancion = "Airplane"; Ya en esta línea estamos asignando un texto a la variable 'cancion'. Para asignarle un contenido a una variable simplemente escribimos un igual después del nombre y después ponemos el contenido seguido de punto y coma para finalizar la 32 sentencia. Como en este caso es un String debe ir entre comillas, si el contenido fuera de números no lo pondríamos entre comillas. Solo los textos los ponemos entre comillas. También podemos inicializar una variable y poner su contenido inmediatamente, en este caso no lo hice para mostrar que podemos crear primero la variable y asignar su contenido luego. Si hubiera querido crear la variable e inicializarla inmediatamente, las dos líneas se hubieran resumido a una así: String cancion = "Airplane"; Aunque este código sea más corto, existen muchas ocasiones en las que no queremos asignar un contenido a una variable hasta más adelante en el código, pero si queremos crear la variable. Estos casos los veremos más adelante en el capítulo llamado 'Ámbitos locales'. Antes usamos System.out.print(); para decir algo que escribíamos en comillas dentro del paréntesis. También podemos poner dentro del paréntesis el nombre de la variable y es el contenido en ese punto de la misma, el que va a salir en la ventana Output. Es precisamente eso lo que hacemos en la siguiente línea: System.out.println(cancion); El resultado en la ventana Output es 'Airplane' ya que este es el contenido que tenemos asociado a la variable cancion en este punto. Después podemos modificar el contenido de la variable, al fin y al cabo es para esto que sirven las variables y es para poder cambiar su contenido a lo largo del código. cancion = "The show must go on"; System.out.println(cancion); Como podemos ver, el código anterior cambia el contenido de la variable a 'The show must go on' y luego imprime nuevamente la misma variable. Observemos 33 que tenemos dos líneas en el código que se ven exactamente iguales, estas dos son la líneas son las que usan System.out.println(); y aunque se vean iguales podemos ver que no generan el mismo contenido como resultado en la ventana de salida. Esto es demasiado importante y es el primer paso para entender la programación, una misma línea de código puede producir resultados muy diferentes. Incluso más adelante veremos que cuando tenemos código que se repite podemos condensarlo en uno solo para que nuestro programa sea más ágil, entre menos código tengamos que escribir para producir diferentes resultados pues mejor. En la última línea cambiamos nuevamente el contenido de la variable pero esta vez no hacemos nada con ella. con esto quiero demostrar algo que puede parecer obvio pero a veces nos puede llevar a errores cuando tenemos códigos largos y complejos. El código se ejecuta de arriba hacia abajo y de izquierda a derecha. Es por eso que podemos poner dos veces el código System.out.println(); pero éste produce resultados diferentes. No es necesariamente un error que cambiemos la variable y al final no hagamos nada con ella, puede ser que el usuario seleccione una nueva canción, y esto haga que la variable se llene con el nombre, pero nunca la haga sonar y por lo tanto la variable nunca cumpla su función. Lo importante es que debemos tener cuidado de cómo se encuentran en dicho momento las variables, debemos estar pendientes si en el punto que queremos la variable está llena con la información que queremos, siempre teniendo en cuenta que el código se ejecuta de izquierda a derecha y de arriba hacia abajo. Podemos crear muchas variables, y si lo queremos, una variable puede ser igual a otra variable y obtendrá el valor de esa segunda variable que tenga asignado en ese momento. Una variable también puede ser igual a otra variable más una operación matemática. Te recomiendo que vayas y modifiques este código a tu gusto para que pruebes diferentes resultados, no hay mejor forma de aprender a programar que programando. 34 Comentarios Aunque en el capítulo anterior hablamos de variables y en el siguiente continuaremos el tema, me pareció pertinente por razones pedagógicas, darle un descanso al cerebro aprendiendo sobre los comentarios en Java que es un tema muy sencillo pero muy útil. Imaginemos que escribimos nuestro reproductor de música, lo terminamos, se lo mostramos a todo el mundo y después de un tiempo nos damos cuenta que queremos modificar algunos comportamientos y agregar nuevas funciones. En este caso volveremos a nuestro código que puede tener hasta 1000 líneas o más y obviamente nos va a costar mucho encontrar porciones específicas del código. Lo más probable es que nos encontremos con porciones de código que aunque fueron escritas por nosotros, no nos acordemos qué hacen. Para eso existen los comentarios en Java, son porciones de texto que no hacen nada útil para la aplicación en sí, pero allí podemos escribir cualquier cosa que queramos para recordarnos a nosotros mismos qué hace cierto código. De hecho los comentarios se hicieron no sólo para que nosotros mismos nos escribiéramos mensajes a nuestro yo del futuro, en muchas aplicaciones es probable que trabajemos en equipo con otros programadores, o que en el futuro otros programadores continúen nuestro trabajo, la mejor práctica en cualquier caso es escribir los comentarios suficientes para hacer el código lo más claro posible. Esto no significa llenar cada línea con comentarios, simplemente significa que por cada porción de código que haga algo específico, por ejemplo mostrar un playlist en nuestro reproductor, podemos poner comentarios cortos y claros como "muestra el playlist en el reproductor" o simplemente "muestra el playlist". Si creemos que hay procedimientos complejos ocurriendo, también podemos hacer anotaciones más específicas en el código. Hay dos tipos de comentarios que usaremos en Java en este proyecto de grado. Podemos hacer comentarios de una línea o comentarios de varias líneas. No existe ninguna diferencia entre los dos tipos de comentario, simplemente se 35 diferencian por el largo en líneas del mismo. Si queremos hacer un comentario corto de una línea procedemos así: // Con dos slash empezamos los comentarios de una línea Cada vez que el compilador encuentra dos forward-slash ignora todo texto que haya en esa línea. Debemos tener cuidado porque deben ser dos de estos // y no dos back-slash como estos \\. Si queremos hacer comentarios más largos usamos los comentarios de varias líneas: /* Este es un comentario de varias líneas, podemos hacer enter y todo este texto será ignorado por el compilador. Para terminar un comentario de varias líneas usamos: */ Para empezar un comentario de varias líneas escribimos /* y para terminarlo usamos el inverso que es */. Los comentarios también se usan para no dejar que un código funcione pero que no queremos borrar para futuras referencias. Por ejemplo imaginemos que escribimos un código que muestra todas las canciones que tenemos en una carpeta. Después de terminar dicho código, se nos ocurre una forma más fácil y más corta de realizar exactamente lo mismo pero no estamos seguros si va a funcionar. Lo mejor que podemos hacer es comentar el código anterior y empezar el nuevo, si después de probar nuestro nuevo código todavía decidimos devolvernos al código anterior, simplemente le quitamos los signos de comentario y así no lo perdemos. Incluso podemos comentar uno de los dos códigos para comparar si se comportan igual o no. 36 Tipos de Variables Las variables en Java pueden tener varios tipos de contenido. En el capítulo de variables vimos como podíamos almacenar texto en comillas dentro de una variable de tipo String. Existen otro tipo de variables llamadas primitivas que contienen otro tipo de información. Existen 8 tipos de variables primitivas, veamos el siguiente código: public class Primitivos{ public static void main (String[ ] args) { boolean esVerdad = true; char c = 64; byte b = 100; short s = 10000; int i = 1000000000; long l = 100000000000L; double d = 123456.123; float f = 0.5F; // Podemos ver todos los resultados con un solo System.out.println() System.out.println(esVerdad + "\n" + c + "\n" + b + "\n" + s + "\n" + i + "\n" + l + "\n" + d + "\n" + f); } } Este código a primera vista puede asustar un poco más, pero en realidad lo que está ocurriendo es muy sencillo. En general lo que tenemos son los 8 tipos de variables primitivas, un comentario y un solo System.out.println() que nos va a mostrar el contenido de nuestras 8 variables usando +. Veamos paso por paso lo que está ocurriendo. Como siempre empezamos declarando nuestra clase y luego nuestro método main(). Dentro de éste último ponemos todo nuestro código por ahora. El primer 37 tipo de variable primitiva que usamos es boolean. Este tipo de primitivos solo pueden tener dos estados: true y false. En nuestro ejemplo del reproductor de música podemos usar este tipo de variables para saber el estado de una función específica que tenga solamente dos estados. Por ejemplo si tenemos un botón que nos permite silenciar el sonido, este botón puede estar asociado a una variable booleana llamada silencio, o cualquiera sea el nombre que escojamos, y su valor puede ser false cuando queremos que nuestra aplicación suene y true cuando queremos que nada suene. Cuando creamos una variable de este tipo pero no le asignamos un valor inmediatamente, por defecto su valor es false. Más adelante veremos que este tipo de variables son muy utilizadas y muy útiles así que no debemos olvidarlas. El segundo tipo de variable es char, que es la abreviación de character. Este tipo de variable sirve para almacenar un solo carácter. No se puede confundir con String ya que char no nos permite almacenar texto, solo nos permite almacenar una letra o signo de cualquier idioma. Este tipo de variable primitiva usa Unicode que es un tipo de codificación estándar que guarda todos los signos y letras usados en las diferentes lenguas de la humanidad y las asocia a un número entre 0 y 65.536. Esto quiere decir que dentro de una variable de tipo char podemos almacenar tanto un número como un signo o letra. En nuestro código usamos el número 64 que es el equivalente al signo arroba. También pudimos haber escrito en vez del número, el signo dentro de comillas sencillas así: '@'. Notemos que en los teclados existen tanto las comillas dobles " " como las comillas sencillas ' '. Para las variables de tipo char debemos usar comillas sencillas. En realidad es raro que en un programa de audio nos encontremos con este tipo de variables. Las variables de tipo byte tienen una capacidad de 8 bits. Esto significa que se usan para almacenar números enteros entre -128 y 127. Son muy útiles cuando hacemos aplicaciones de audio ya que por lo general cuando vamos a manipular, crear o analizar la información en la que está guardada el audio, debemos usar este tipo de variables para almacenar nuestra información de audio y así poder 38 hacer algo con ella. Lo mismo ocurre cuando estamos trabajando con MIDI, la información que se envía y se recibe está expresada en bloques de a 8 bits, así que cuando queremos crear mensajes MIDI, la mejor opción es usar este tipo de variables. En la siguiente variable encontramos el tipo short que usa 16 bits. Esto quiere decir que permite almacenar números enteros entre -32.768 y 32.767. Pensemos que cuando tenemos la calidad de CD, se usa una profundidad de 16 bits en el audio. Esto quiere decir que tenemos toda esta cantidad de valores para almacenar una exacta amplitud de onda en un determinado momento y así y todo muchas personas creen que no es suficiente y deciden irse por usar calidad de audio de hasta 24 bits. No pienso entrar en esta discusión sobre calidad de audio digital, más adelante hablaremos un poco más sobre procesamiento de audio digital. Por ahora simplemente pensemos que con 16 bits o en un tipo short, podemos poner una muestra de audio que use esta profundidad de bits. En la siguiente variable encontramos el tipo int que es la abreviación de integer. Este tipo de variable almacena 32 bits. y se pueden poner valores entre 2.147.483.648 y 2.147.483.647. Estos números son lo suficientemente elevados para casi cualquier aplicación, tal vez tendríamos problemas si estamos creando una calculadora en Java pero de resto casi siempre una variable de tipo int es más que suficiente. Son muchas las ocasiones en las que podemos usar un variable de este tipo en audio. Pensemos nuevamente en nuestro ejemplo de un reproductor de audio, si por ejemplo queremos que nuestra aplicación cuente la cantidad de canciones que tiene el usuario, es probable que debamos usar una variable int. La variable de tipo long usa 64 bits y como te imaginarás las cantidades son demasiado grandes como para mencionarlas. Son números enteros que ni sé cómo leer así que con que sepas que cuando un int se te quede corto puedes usar long. La verdad es que son cantidades tan grandes y usa tantos bits que es raro ver este tipo de variables en una aplicación. Como podemos ver en nuestro 39 ejemplo, al final del número debemos agregar una L, esto es debido a que Java trata todos los números grandes como int para proteger el sistema de usar demasiados recursos. Cuando usamos un long, Java nos pide que agreguemos una L al final del número para que estemos seguros que queremos usar un long y no un int. Por último tenemos dos tipos de variables primitivas que son las que nos permiten almacenar números decimales, esto son double y float. La diferencia principal es que double usa 64 bits y float usa 32 bits. Java trata todos los decimales como float así que cuando queremos usar un float, debemos asegurarnos de agregar una F al final del número. Aunque double usa 64 bits, que es una cantidad grande, esto es necesario porque las posibilidades al usar decimales son muchas. Por lo general el volumen en una aplicación de audio, es manejado por un slider que da números decimales donde 1 es el volumen máximo y 0 es silencio total. Las variables de tipo byte, short, int, long, float y double están hechas para almacenar números. Aunque char puede almacenar números, este no es su fin sino asociar dichos números con letras. Es probable que te estés preguntando ¿Por qué usar 6 tipos diferentes de variables para almacenar números? La respuesta es que debemos pensar en crear aplicaciones rápidas. Pensemos que una variable tipo long usa 64 bits para almacenar números, a diferencia de una variable de tipo byte que usa solo 8 bits, si estamos creando una variable de la cual sabemos que su contenido nunca va a llegar a más de 127, para qué vamos a sobrecargar nuestra aplicación haciéndola usar más bits de los necesarios, en este caso escogemos byte y no long. Siempre que creemos una variable y en general siempre que estemos programando, debemos pensar en la velocidad de nuestras aplicaciones y su óptimo rendimiento. De cualquier forma debemos tener mucho cuidado porque si tratamos de almacenar un número que excede la capacidad del tipo de su variable, el código no compilará en el mejor de los casos, en el peor de los casos el código compilará y habrán errores en el transcurso de 40 nuestra aplicación que pueden terminar causando errores graves o trabando nuestra aplicación. Al final de nuestro método main() tenemos un System.out.print() que imprime todas nuestras variables. Podemos agregar un + para concatenar resultados. Concatenar es el término que se usa en programación para decir que se encadenan o unen resultados. Debemos tener cuidado porque con el signo + podemos sumar dos números o simplemente visualizar el resultado de los dos por aparte. Supongamos que tenemos dos variables que contienen números, si escribimos System.out.print(variable1 + variable2) el resultado será la suma de los dos números. Si lo que queremos es ver ambos resultados por aparte sin que se sumen, lo que podemos hacer es poner en la mitad un String que recordemos que es texto entre comillas así System.out.print(variable1 + " " + variable2). En el caso anterior estamos agregando un texto que no es más que un espacio, éste va a permitir que se muestren los resultados separados por un espacio y no se sumen. Entonces en nuestro código original estamos uniendo resultados con el texto "\n" que lo que hace es simular un salto de línea, esto es como cuando hacemos enter en un procesador de texto. siempre que queramos hacer un salto de línea usamos dentro de un String el código \n. Como puedes ver, en el código original hice enter en medio del código que imprime el resultado porque no cabía el texto y terminé en la siguiente línea, esto no es problema ya que Java ignora la cantidad de espacio en blanco. Esto se puede hacer siempre que estemos fuera de comillas. Aquí hemos visto las variables primitivas, pero notemos que dentro de éstas no está la variables de tipo String que también es un tipo de variable válido. Lo que pasa es que String no es un tipo primitivo sino es un objeto. Por ahora no pretendo complicar el asunto, lo importante es que entiendas que existen los objetos y que los diferenciamos porque empiezan en mayúscula, más adelante veremos qué son los objetos. Observa que ninguno de los tipos primitivos empieza en mayúscula. Entonces las variables también pueden ser del tipo de objetos que son muchos y hasta tú puedes crearlos, pero esto lo veremos más adelante. 41 Arreglos Imaginemos que necesitamos una variable que pueda albergar varios contenidos a la vez. En mi experiencia personal me he dado cuenta que casi toda aplicación necesita variables de este tipo. Por ejemplo cuando he creado juegos que enseñan música, necesito crear una variable que contenga todos los nombres de notas como Do, Re, Mi, Fa, etc. En estos casos usamos los arreglos. Veamos cómo se escribe un arreglo que contenga todos los nombres de notas sin sostenidos o bemoles: public class Arreglos { public static void main(String[ ] args) { String[ ] nombresDeNotas = new String[7]; nombresDeNotas[0] = "Do"; nombresDeNotas[1] = "Re"; nombresDeNotas[2] = "Mi"; nombresDeNotas[3] = "Fa"; nombresDeNotas[4] = "Sol"; nombresDeNotas[5] = "La"; nombresDeNotas[6] = "Si"; System.out.println(nombresDeNotas[0]); } } Como ya debemos tener claro, primero creamos una clase y luego el método main(). En la primera línea del método principal inicializamos un nuevo arreglo así: String[ ] nombresDeNotas = new String[7]; Si analizamos cuidadosamente este código veremos que se parece mucho a cuando creamos una variable. Lo primero que tenemos es el tipo de contenido que va a contener nuestro arreglo, en este caso son cadenas. Después del tipo 42 escribimos un corchete que abre y en seguida uno que cierra, esta es la indicación que le damos a Java para decirle que estamos creando un arreglo y no una variable normal. Después de los corchetes escribimos el nombre que le queremos asignar al arreglo, este nombre debe seguir los mismos parámetros que las variables. Luego ponemos un signo igual y escribimos new String[7]; que es el código necesario para declarar que el contenido de este arreglo es de 7 elementos del mismo tipo especificado anteriormente que es un String. Si el arreglo hubiese sido de tipo int entonces escribiríamos así: int[ ] arregloDeNumeros = new int[7]; En este ejemplo creamos un arreglo de tipo int con 7 elementos de contenido pero todavía no hemos especificado qué contenido va en cada una de esas 7 casillas. Volviendo a nuestro código original, en las siguientes líneas especificamos el contenido de cada casilla, veamos la primera: nombresDeNotas[0] = "Do"; Con este código estamos asignando el texto "Do" a la primera de las siete casillas de nuestro arreglo llamado nombresDeNotas. Dentro de los corchetes escribimos la casilla en la que vamos a meter un contenido, pero debemos ser cuidadosos porque estas casillas no se nombran desde el número 1 sino desde 0. Entonces la primera casilla es 0, la segunda es 1, la tercera es 2 y así sucesivamente. Después de especificar la casilla simplemente escribimos el signo igual y luego el contenido seguido de punto y coma para aclarar que acabamos una sentencia. No olvidemos que toda sentencia lleva punto y coma, tampoco olvidemos que las clases y los métodos en sí no son sentencias y por eso no llevan punto y coma. En el código original puedes ver que de la misma forma se terminan de asignar los contenidos a las 7 casillas del arreglo, estas son las casillas de la 0 a la 6, para un total de 7 casillas. 43 En nuestro System.out.println(nombresDeNotas[0]); estamos imprimiendo la casilla 0, si queremos imprimir otra casilla simplemente cambiamos el número dentro de los corchetes por cualquier otro número válido de casilla. Quiero aclarar que en la creación de los arreglos podemos poner los corchetes después del tipo o después del nombre del arreglo, ambas notaciones son válidas. También pudimos haber asignado los valores a las casillas inmediatamente en el momento de creación del arreglo de la siguiente forma: String[ ] nombresDeNotas = {"Do", "Re", "Mi", "Fa", "Sol", "La", "Si"}; Esta línea produce exactamente el mismo resultado que nuestras primeras 8 líneas dentro del método principal. Aunque esta es mucho más corta, hay ocasiones en las que necesitamos primero crear el arreglo y más adelante en el código asignar su contenido, así que debemos aprender las dos formas. Como ya vimos antes, podemos crear arreglos de tipos primitivos y también arreglos de objetos como String. Más adelante veremos qué son los objetos. Si en algún momento queremos saber cuántas casillas tiene un arreglo, podemos usar el código nombresDeNotas.length e igualarlo a una variable para guardar el número así: int largoDeArreglo = nombresDeNotas.length; Como muchos de los temas que expongo aquí, debo tratar de ser lo más breve posible y por eso cuento lo fundamental, pero te aseguro que hay muchos libros y mucha documentación en línea que te explica más a fondo los arreglos y temas que quieras profundizar. Por ahora esto es todo lo que debemos saber. Por ejemplo, si eres curioso puedes buscar en Internet sobre los arreglos multidimensionales en Java que te permiten agregar varios resultados en una misma casilla, son como casillas dentro de casillas y son muy útiles. 44 Matemáticas Como lo he dicho antes, la programación requiere de un pensamiento muy matemático. Gracias a la matemática podemos ahorrar mucho tiempo programando nuestras aplicaciones. Cuando estemos viendo la parte de audio aplicaremos matemáticas más avanzadas como funciones seno y coseno para poder crear ondas, pero este tema es tan amplio que podríamos llegar a profundizar en temas como derivadas rápidas de Fourier para analizar nuestro audio. En este proyecto de grado pretendo hacer un primer acercamiento a las generalidades de la programación en Java, pero no olvidemos que algunos de los temas que trato aquí, son en realidad incluso maestrías completas para los programadores, así que no puedo pretender incluirlo todo. Lo importante es que al leer este capítulo de matemáticas tengas claras algunas nociones básicas para que luego vayas y aprendas más por otros medios. Veamos el siguiente código que hace las cuatro operaciones básicas en matemáticas: public class Math { public static void main(String[] args) { int num1 = 134; int num2 = 60; // suma int suma = num1 + num2; // resta int resta = num1 - num2; // multiplicación. Se usa el símbolo * int multi = num1 * num2; // división usando double double division = (double) num1 / num2; // Imprime el resultado que quieras, en este caso división System.out.println(division); } } 45 El código anterior no necesita mucha explicación ya que lo puedes entender mirándolo atentamente. Simplemente creamos dos variable de tipo int y luego realizamos las cuatro operaciones básicas con ellos: suma, resta, multiplicación y división. Cada uno de estos resultados los almacenamos en una nueva variable. Es bueno que nos demos cuenta que si queremos cambiar los números a probar solo tenemos que cambiarlos en las variables llamadas num1 y num2. Si bien hubiésemos podido escribir en cada operación los números en vez de las variables, lo bueno de haber puesto variables para los números a probar es que podemos cambiarlos en la variable e inmediatamente se actualizarán para todas las operaciones. Esto es básico en programación y debemos usarlo a nuestro favor. En este caso estamos haciendo 4 operaciones con los mismos números, pero imaginemos que tenemos 30 operaciones diferentes para los dos mismos números, si en ese caso no usáramos variables, para cada una de las 30 operaciones tendríamos que cambiar los números si queremos ver un nuevo resultado. Lo bueno de usar variables es que si queremos cambiar el contenido en las 30 operaciones, simplemente cambiamos el contenido de las dos variables.. También veamos que para todas las variables escogimos como tipo int excepto para la división donde usamos double. Aunque los valores que usamos en este ejemplo son muy pequeños y pudimos usar incluso el tipo short, decidí usar int para que modifiques los valores a tu gusto si quieres usar valores mucho más grandes. Como estamos usando números enteros en num1 y num2, entonces podemos estar seguros que la multiplicación, suma y resta también nos va a dar números enteros. El problema es cuando usamos la división. si usáramos int para la división, el resultado sería un número entero siempre y cortaría nuestros decimales. Entonces debemos tener cuidado y poner de tipo double para la división. Con solo poner double no logramos el resultado deseado en decimales porque num1 y num2 son enteros, entonces Java piensa que debe devolver un número entero, para cambiar este comportamiento usamos el modificador de tipo 46 (double) que anteponemos a nuestra operación y ahora si veremos el resultado deseado. Los modificadores de tipos o la conversión de tipos es algo muy frecuente en Java. Muchas veces tenemos un tipo que por muchas razones debemos convertir a otro. Más adelante veremos en el capítulo llamado 'Conversión de tipos' cómo podemos pasar de un tipo a otro. Por ahora sepamos que existen y que en operaciones con números es probable que los tengamos que usar cuando los tipos no son los esperados como en el caso de la división. Ya sabemos entonces que para hacer operaciones entre dos números simplemente ponemos el símbolo de la operación deseada en la mitad de los dos. Si necesitamos operaciones más complejas entonces simplemente podemos usar paréntesis para asegurarnos que algunas cosas ocurran primero como en el siguiente código: double operacion = (double) ((num1 + num2) /(num1 - num2)) * (num1 * num2); En el caso anterior usamos paréntesis para asegurarnos que algunas operaciones ocurran primero que otras. Como en matemáticas, las operaciones se resolverán desde los paréntesis más internos hasta los más externos. Como no sabemos si el resultado puede ser un decimal entonces nos aseguramos almacenando el contenido en una variable de tipo double y además escribimos el modificador de tipo (double) antes de toda la operación. Muchas veces queremos aumentar o disminuir en uno el contenido de una variable. Podemos usar la siguiente expresión para hacerlo de forma rápida: num1 ++; 47 Con este código hacemos que el contenido de num1 se incremente en 1. Si el contenido era 134, ahora es 135. También podemos usar en vez de dos signos +, dos signos - y así el contenido de la variable disminuirá en uno. Una operación muy usada en programación es la operación módulo. Recordemos que la operación módulo es la que nos muestra el residuo de una división. Pensemos que estamos creando un metrónomo. Si por ejemplo queremos que nuestro metrónomo cuente hasta cuatro en cada compás para decir el número de pulsos en cuatro cuartos, podemos usar la operación módulo de la siguiente forma. Pensemos que cada vez que nuestro metrónomo suena, tenemos una variable que se incrementa usando ++ como vimos antes. Supongamos que esta variable se llama contador y se inicializa en cero así: int contador = 0;. Con cada sonido aumenta así: contador ++;, En este punto no tenemos los conocimientos suficientes para crear un código que nos permita crear un metrónomo como tal y probar este código, pero usemos la imaginación y este código muy sencillo para probarlo: public class Modulo{ public static void main(String[] args) { // el número de sonidos que ha hecho el metrónomo int contador = 1245; // imprime el pulso actual en un compás de cuatro pulsos System.out.println((contador % 4) + 1); } } Debemos usar un poco la imaginación para probar este código. Imaginemos que la variable contador se incrementa en uno cada vez que el metrónomo suena. Para probar el código asigna el número que quieras a la variable contador y mira 48 que el resultado en la ventana de salida siempre va a ser un número entero del 1 al 4. Si por ejemplo decimos que contador es igual a 5, el resultado será 1, si igualamos la variable a 6 el resultado será 2 y así sucesivamente como muestra la siguiente tabla. contador System.out.println((contador % 4) + 1) 1 1 2 2 3 3 4 4 5 1 6 2 7 3 8 4 9 1 Como puedes ver los resultados en la ventana de salida siempre son los números del 1 al 4 en orden si la variable contador aumenta de a uno en uno. En este caso lo que hicimos fue utilizar la operación módulo que en Java se representa con el símbolo %. El código que usamos para lograr este resultado es ((contador % 4) + 1) que significa lo mismo que dividir contador entre 4 y luego obtener solo el residuo de la división, después tomar ese número y sumarle 1. Esta es la forma en que se hacen los relojes y cronómetros en un lenguaje de programación. Por ejemplo usamos la operación módulo para crear los segundos de un reloj, que como sabemos van de 0 a 59. Lo que hacemos es sacar el módulo del número de segundos que han transcurrido contra 60. El resultado son los números del 0 al 59 ordenados. En el caso del contador de tiempos del compás tuvimos que sumarle 1 a cada resultado sino el programa nos hubiera devuelto los números del 0 al 3. 49 Muchas veces necesitamos un número aleatorio. Por ejemplo yo los he usado mucho porque me gusta que mis juegos empiecen siempre de forma aleatoria con preguntas diferentes. Para crear un número aleatorio usamos: double aleatorio = Math.random(); Si usamos un System.out.println para ver la variable aleatorio vemos que cada vez que ejecutamos el programa saldrá un número diferente. Este es un número entre 0 y casi 1 sin devolver nunca 1. Si quisiéramos tener números entre 0 y 45 simplemente tendríamos que multiplicar Math.random() por 46. No puede ser por 45 porque recordemos que Math.random() nunca devuelve 1. Si quisiéramos números aleatorios entre 30 y 50 tendríamos que multiplicar por 21 que es la cantidad de números entre 30 y 50 más uno y al final sumarle al resultado 30 que es el número mínimo así: (Math.random() * 21) + 30 En realidad todo lo que podemos hacer en matemáticas es demasiado para abarcarlo todo aquí. En el camino veremos otras posibilidades. Más adelante veremos un capítulo dedicado a aprender a buscar documentación que nos sea útil en Internet sobre Java. Les puedo dejar como inquietud que con Java podemos redondear números a su entero hacia arriba o hacia abajo más cercano, hacer operaciones con seno, coseno y tangente, sacar logaritmos, hacer exponentes, sacar raíz cuadrada y cualquier operación que se nos ocurra matemáticamente. Esto es muy útil porque todo procesador o analizador de audio está basado en algoritmos matemáticos. Como Java nos permite mirar uno a uno los bits que están en un archivo de audio y como Java nos permite manejar matemáticamente estos bits, esto quiere decir que podemos desarrollar con Java casi que todos los programas para procesar audio que se nos ocurran. Aunque hay que tener en cuenta un punto del que hablaremos más y es la latencia. Pero de eso hablaremos más adelante. 50 Sentencias de prueba 'if' En el capítulo sobre variables primitivas hablamos de los tipos boolean. Un booleano es simplemente uno de dos estados posibles: true o false. Pensemos en el ejemplo de un reproductor de audio. Recordemos que habíamos dicho que una variable de tipo boolean es muy útil para almacenar la información de silencio del audio en nuestro programa. Podemos crear una variable llamada silencio cuyo estado es true cuando el usuario hace clic en el botón mute. Para continuar con este código, obviamente no es suficiente modificar simplemente la variable para silenciar el programa. También necesitamos que el bloque que se encarga del sonido no se ejecute cuando la variable es igual a true y que solo se ejecute cuando la variable es igual a false. En estos casos usamos una sentencia de prueba if. Éstas no son más que bloques de código que se ejecutan cuando algo es true. Veamos un código muy simple que demuestra cómo sería en términos generales el código de silencio para un reproductor de audio: public class SentenciaIf { public static void main(String[] args) { // Cambia a true para silenciar la aplicación boolean silencio = false; // Sentenica de prueba if if (silencio == true) { // código que silencia el audio System.out.println("El programa no suena."); } else { // código que hace sonar el audio System.out.println("El programa suena."); } } } 51 El código anterior usa una sentencia de prueba para ejecutar un código cuando la aplicación esté silenciada y otro diferente cuando queremos que la aplicación suene. Aunque todavía no tengamos los conocimientos para manejar audio en Java, este tipo de conocimientos básicos son necesarios para que podamos programar aplicaciones con audio. En el momento que sepamos cómo hacer para que suene audio en Java, simplemente agregamos ese código dentro del bloque que tiene el comentario // código que hace sonar el audio y cuando sepamos cómo hacer para silenciar todos los sonidos, ponemos ese código dentro del bloque que tiene el comentario // código que silencia el audio. Más adelante también aprenderemos cómo hacer para crear interfaces gráficas con botones, allí podremos asociar el botón con el estado de la variable de tal forma que cada vez que el usuario lo presione, la variable silencio pase de un estado al otro. De forma muy simple, una sentencia de prueba if dice: si esto es verdad entonces corre el primer bloque de código. En términos muy simples funciona así: if (variable == true) { // código para true } Una sentencia de prueba if puede ser así de simple. Estas sentencias empiezan con la palabra if seguida de un código en paréntesis que es el encargado de probar si algo es verdad para continuar con el bloque de código entre llaves. Si el código entre paréntesis devuelve true, entonces el bloque se ejecutará. En este caso estamos suponiendo que tenemos una variable llamada variable y cuando usamos dos signos igual == estamos diciendo: compara si lo que está antes es igual a lo que está después de los iguales. En este caso estamos comparando si variable es igual a true. Debemos tener mucho cuidado porque deben ser dos iguales así == y NO uno solo para poder comparar. También pudimos haber comparado variables que no sean del tipo boolean. Para comparar una variable de tipo String no usamos dos iguales sino que usamos variable.equals("xxx"): 52 if (variableString.equals("Cualquier texto que queramos comparar")) { // código que se ejecuta si la comparación es verdad } En el código anterior estamos comparando el contenido de variableString con uno creado por nosotros, si son exactamente iguales entonces el bloque se ejecuta, si no son iguales entonces no pasa nada. También podemos comparar variables que contengan números usando nuevamente los dos iguales: if (variableNumeros == 123) { // código que se ejecuta si la comparación es verdad } Recordemos que en el caso de los números no usamos comillas. En el caso de las variables de tipo boolean no es necesario igualar a true, podemos igualar a false de la siguiente forma: if (variableBoolean == false) { // Si variableBoolean } Con las variables de tipo booleano, cuando queramos comparar si ésta es verdad, no es necesario escribir los dos iguales y luego true, en realidad es redundante y podemos probar así: if (variableBoolean) { // Si variableBoolean es true } El código anterior es exactamente igual a escribir: 53 if (variableBoolean == true) { // '== true' es redundante } Aunque si lo queremos, no es un error escribir la redundancia. Ahora volvamos a nuestro código original sobre silenciar nuestro programa de audio. Veamos que lo primero que tenemos es una variable que podemos poner en true o false y dependiendo de esto, vamos a ver resultados diferentes en la ventana de salida. Luego probamos de forma redundante si la variable silencio es igual a true para ejecutar un código específico, pero notemos que después del primer bloque tenemos la palabra else seguida de otro bloque de código. Este segundo bloque de código es el que se ejecutará si la condición escrita entre paréntesis no fue verdadera. De forma general podríamos pensarlo así: if (true) { // código para true } else { // código para false } El código anterior y todas las sentencias de prueba if se pueden leer así: si el código entre paréntesis es verdad ejecuta el primer bloque. El segundo bloque es opcional y si lo escribimos significa: si el código entre paréntesis no fue cierto entonces ejecuta este segundo bloque. Hay ocasiones en las que necesitamos hacer varias pruebas porque no siempre las variables tienen solo dos estados. Por ejemplo las variables que contienen números pueden tener muchos estados, en este caso podemos hacer muchas más cosas como muestra el siguiente código de prueba: 54 if (numero == 10) { // código cuando número es 10 } else if (numero < 10){ // código cuando número es menor que 10 } else { // código cuando número no cumple ninguna de las condiciones anteriores } En el código anterior tenemos tres bloques. El primero prueba si la variable numero es igual a 10. En el segundo bloque tenemos una variación posible de la prueba else a la cual le agregamos un if, esto quiere decir: si la primera prueba entre paréntesis no se cumplió entonces probemos si esta segunda si se cumple. Si la primera prueba se cumple, se ejecuta el primer bloque y los otros ni siquiera se prueban. En el paréntesis de la prueba else if ya no estamos probando si la variable numero es igual a algún valor sino estamos probando si es menor que 10. Los signos para comparar más usados son los siguientes: Menor que < Mayor que > Menor o igual que <= Mayor o igual que >= Igual que == No es igual que != El signo de admiración ! significa negación y lo podemos usar para probar si una variable de tipo boolean no es verdadera así: (!variableBoolean) al anteponer el signo de admiración es como decir: si variableBoolean es igual a false entonces ejecuta el siguiente bloque. Terminando nuestro anterior código con tres bloques, en el tercero tenemos el código que se ejecutará si ninguna de las dos pruebas fue positiva, en este caso si la variable numero es mayor que 10 entonces el tercer bloque correrá. Las pruebas if no terminan en punto y coma pero las sentencias dentro de sus bloques sí. 55 Para terminar las sentencias de control, hay varias ocasiones en las que necesitamos probar más de un estado a la vez. Por ejemplo cuando necesitamos ejecutar un bloque de código cuando un número se encuentra entre 50 y 100 podemos proceder así: if (numero >= 50 && numero <= 100) { // ejecuta este bloque si el número está entre 50 y 100 } Los dos signos && significan 'y' que nos permite unir dos afirmaciones. En este caso entre el paréntesis estamos diciendo: si la variables numero es mayor o igual que 50 Y si la variable numero es menor o igual que 100 entonces ejecuta el siguiente bloque de código. En otras ocasiones no necesitamos unir dos afirmaciones sino saber si una entre varias es cierta, para eso usamos dos barras verticales seguidas ||. Éstas significan 'o', es como decir si esto O lo otro es cierto ejecuta el bloque. if (numero == 100 || numero == 200) { //Si el número es 100 o si es 200 ejecuta el bloque } Este paréntesis dice: si la variable numero es igual a 100 Ó si la variable numero es igual a 200 ejecuta el bloque de código. En varias ocasiones necesitamos hacer muchas pruebas en un mismo paréntesis que involucren tanto && como ||, esto lo podemos hacer y nos ayudamos de más paréntesis para hacer varias pruebas. Por ejemplo para probar una variable num que sea igual a 30 ó igual a 130 ó esté entre 50 y 100: if ((num == 30 || num == 130) || (num >= 50 && num <= 100)) { // código } 56 Ciclos Aunque hay mucha información hasta este punto y aún no hemos tocado el tema del audio en sí, es necesario tener claros estos conocimientos básicos para poder programar aplicaciones de audio en Java. Personalmente cuando aprendí los primeros lenguajes de programación pensaba que no iba a lograr aprender tantas nuevas palabras y sintaxis pero la verdad es que cuando terminaba un curso o cuando terminaba de leer un libro de programación y luego me sentaba en el computador a programar, ahí me daba cuenta que había aprendido mucho más de lo que creía y de ahí en adelante era simplemente sentarme a pensar cómo crear códigos efectivos que hicieran algo particular. Es en la experiencia que de verdad aprendemos el lenguaje. Estoy seguro que lo mismo te pasará a ti, aunque el proceso pueda tener partes tediosas, hay una gran recompensa cuando empiezas a crear tus primeras aplicaciones. Hay muchos códigos diferentes que hacen exactamente lo mismo, lo importante es tratar de pensar siempre cómo programar los códigos más efectivos y simples en cuanto se pueda. El tema que veremos en este capítulo son los ciclos, que siempre son de gran ayuda para no reescribir código innecesariamente y por lo tanto hacer códigos más efectivos. Antes de empezar con los ciclos quiero que pensemos un poco en lo que hemos aprendido hasta aquí para que veas que has aprendido bastante y para mantener tus pensamientos sobre Java ordenados. Primero vimos que para escribir un código en Java simplemente tenemos que tener una estructura básica clara que es la anatomía del lenguaje que empieza con la creación de una clase y un método principal que es donde estamos escribiendo todo nuestro código por ahora. Dentro del bloque del método principal podemos escribir todas las sentencias que queramos, cada una de ellas debe terminar en punto y coma. Dentro de los bloques podemos crear variables que pueden ser de diferentes tipos, ya sean primitivas u objetos como String. Dentro de nuestro código podemos escribir comentarios para mantener el código claro. Cuando necesitemos una variable que albergue varios valores usamos los arreglos. También sabemos que 57 podemos hacer todas las operaciones matemáticas que queramos en Java y sabemos cómo hacer algunas operaciones básicas. Por último vimos que existen las sentencias de control if que nos ayudan a ejecutar códigos basados en condiciones específicas. Si lo piensas así verás que vas muy bien y en este punto podemos empezar a acelerar el aprendizaje. En realidad lo más tedioso del proceso ya pasó, a partir de este punto el proceso será mucho más claro y agradable. Los ciclos son simplemente bloques de código que se repiten tantas veces como queramos. En todos los programas son muy útiles. En audio son una excelente ayuda en el siguiente escenario: imaginemos que estamos tratando de crear una onda cualquiera que dura 2 segundos con una resolución de 44100 muestras por segundo. Esto quiere decir que tendríamos que escribir 88200 líneas de código para llenar cada una de las muestras en los dos segundos. ¿Quién escribe 88200 muestras? El que no haya leído este capítulo de ciclos en Java. Veamos cómo sería fuera de contexto un ciclo que se repitiera 88200 veces: for (int i = 1; i <= 88200; i++) { System.out.println(i); } Si escribimos el código anterior en su respectivo contexto dentro de su método principal en una clase, vamos a obtener 88200 líneas en la ventana de salida, cada una contando los números desde 1 hasta 88200. Obviamente para hacer una onda tendríamos que agregar un par de cosas, pero créanme, son muy pocas las líneas que van dentro del bloque anterior para hacer una onda seno de una frecuencia específica. Más adelante veremos cómo hacerlo y para eso necesitamos entender cómo funcionan los ciclos así que continuemos. Con lo anterior dicho no podemos dudar del poder de los ciclos. Existen tres tipos principales de ciclos en Java. Empecemos con el ciclo que acabamos de usar. 58 El ciclo for viene en dos presentaciones, la que acabamos de usar y otra que veremos más adelante. Así como lo acabamos de usar sirve para hacer repeticiones un número de veces específicas que de antemano sabemos cuántas son. En este caso ya sabíamos que necesitábamos 2 segundos a 44100 muestras por segundo, para un total de 88200 muestras así que este ciclo era útil. Estos ciclos funcionan a partir de la creación de una variable dentro del paréntesis que le sigue a la palabra for, dentro del paréntesis vamos a escribir tres sentencias separadas por punto y coma: 1. Inicializamos la variable. int i = 1; 2. Escribimos una condición que debe cumplirse para que los ciclos continúen, cuando esta condición deje de cumplirse el ciclo terminará. La condición se escribe como cuando hablamos de las sentencias de control. En este caso quiere decir mientras la variable llamada i sea menor o igual a 88200. i <= 88200; 3. Escribimos lo que queremos que ocurra en cada repetición con dicha variable. En este caso y por lo general queremos que la variable aumente en uno con cada repetición. i++; Las tres condiciones anteriores van dentro del paréntesis. Luego ponemos unas llaves para indicar el código que queremos que ocurra en cada repetición, podemos escribir varias sentencias separándolas con punto y coma, en este caso solo escribimos una que es un System.out.println(i). Veamos que en esta sentencia para imprimir en la pantalla de salida, usamos nuevamente la variable i para saber en qué número de línea, o en qué número de repetición vamos. Resumiendo en poco este primer ciclo for, primero escribimos la palabra for, luego ponemos entre paréntesis ( ) las tres condiciones antes mencionadas y por último después del paréntesis abrimos un bloque { } en el que pondremos el código que queremos que se repita durante el ciclo, normalmente se usa la variable del paréntesis dentro de este bloque. 59 El segundo tipo de ciclo también es for y su estructura es muy parecida al ciclo anterior, pero se diferencian por el contenido que ponemos dentro del paréntesis. Este segundo tipo de ciclo se usa con arreglos. Recordemos nuestro arreglo de las notas musicales y esta vez imprimamos en la ventana de salida todo el contenido de nuestro arreglo: String[ ] notasMusicales = {"Do", "Re", "Mi", "Fa", "Sol", "La", "Si"}; for (String nota : notasMusicales) { System.out.println(nota); } Si ponemos este código en su entorno correcto, esto quiere decir dentro de un método principal por ahora, vamos a ver en la ventana de salida cada una de las notas musicales en una línea diferente. En la primera línea creamos el arreglo y luego tenemos el ciclo que como vemos es muy parecido en su forma al ciclo anterior. La diferencia es que esta vez tenemos dentro del paréntesis una sola sentencia que nos pide lo siguiente: una variable que contenga temporalmente cada una de las casillas del arreglo que en este caso es String nota que creamos con el nombre nota para poder ser más descriptivos, luego escribimos dos puntos para en seguida indicar en qué arreglo queremos mirar su contenido para hacer el ciclo. Dentro de las llaves tenemos el bloque que se va a repetir en cada vuelta del ciclo, en este caso el ciclo durará el largo del arreglo y con cada vuelta las casillas empezarán a entrar en orden a la variable que hemos creado, en este caso nota, para luego imprimirse una a una en la sentencia del bloque. Este tipo de ciclos se usan para hacer algo con cada uno de las casillas de un arreglo, por lo tanto el largo del ciclo está determinado por el largo del arreglo. Los dos ciclos for vistos anteriormente se usan cuando sabemos la cantidad de repeticiones o podemos llegar a averiguarlas al menos. Si estamos seguros que necesitamos un ciclo de 30 vueltas pues escogemos el primer tipo de ciclo for. Si en cambio necesitamos un ciclo que dure el largo de un arreglo entonces 60 escogemos el segundo. Cuando no podemos saber de antemano la cantidad de vueltas usamos el tercer tipo de ciclos llamado while. Los ciclos while se parecen mucho en su estructura a las sentencias de control if. Pensemos que un ciclo while es simplemente una sentencia if que repite el contenido de su bloque mientras la condición dada sea cierta. Estos ciclos son muy buenos cuando no sabemos qué tantas vueltas necesitamos. Imaginemos que seguimos creando nuestro reproductor de música y en un punto queremos buscar entre nuestra lista de todas las canciones una de Rock. Debido a que no sabemos en cuántas canciones tengamos que mirar hasta encontrar la correcta, es buena idea usar un ciclo while. El siguiente sería el código del ciclo para encontrar la canción de Rock. String[ ] lista = {"Pop", "R&B", "Soul", "Trance", "Techno", "Rock", "Funk"}; String genero = ""; int cancion = 0; while (!genero.equals("Rock")) { genero = lista[cancion]; cancion ++; } System.out.println(genero + " está en la casilla: " + (cancion - 1)); En el código anterior primero creamos una lista de géneros que podemos modificar a nuestro antojo y el resto del código siempre encontrará la palabra "Rock". Luego creamos una variable llamada genero que inicialmente es igual a nada, pero luego vamos a igualarla dentro del ciclo a cada uno de los ítems de la lista y los vamos a comparar hasta que obtengamos la palabra "Rock". El paréntesis básicamente dice 'Mientras la variable genero NO SEA igual a Rock corre el siguiente bloque'. Recordemos que el signo ! significa negación. Recordemos que para comparar dos textos usamos string1.equals(string2). Usamos la variable cancion para entrar a cada uno de los elementos en el arreglo. 61 Antes no habíamos usado esta técnica, pero veamos que podemos poner dentro de los corchetes del arreglo una variable y esto es totalmente válido. Estudia y modifica el código anterior hasta que lo entiendas. Hay formas más fáciles y cortas de en código de hacer la aplicación anterior pero lo importante que quiero es que entiendas el funcionamiento de while. Un ciclo while es simplemente así: while (true) { // código mientras algo sea verdad } Un ciclo while es una condición que debe mantenerse dentro del paréntesis para que el ciclo siga. Cuando la condición ya no es verdad el ciclo se detiene. Es importante entender que el código, como ya lo mencionamos antes, se ejecuta de arriba hacia abajo, esto quiere decir que cuando llegamos a un ciclo, el código no continuará ejecutándose hasta que el ciclo termine. A veces queremos parar un ciclo en medio de su ejecución. Esto puede pasar por muchas razones. Pensemos que estamos creando ondas que duren dos segundos pero queremos que en realidad se escriban los ciclos completos de las ondas y no nos queden ondas a medias, en este caso podemos parar el ciclo justo cuando termina la última onda de escribirse antes de completarse los dos segundos, esto puede ocurrir antes y no exactamente cuando ocurren los dos segundos exactos y por eso podemos querer parar el ciclo una pequeñísima fracción de segundo antes. Para esto podemos usar el código: break; Simplemente escribimos este código dentro del bloque del ciclo donde queremos parar el mismo. Debemos escribirlo dentro de una sentencia de prueba if porque si está suelto simplemente parará el ciclo en su primera vuelta y solo queremos que ocurra en casos especiales que por alguna razón especial queremos detenerlo. 62 Métodos Es hora de empezar a escribir código fuera del método main(). Además del método principal podemos escribir otros métodos creados por nosotros que se ejecutarán cuando queramos. Éstos sirven para organizar nuestro código y de ahora en adelante los vamos a usar bastante. Imaginemos si tuviéramos que escribir todo nuestro código en el método principal, sería muy desordenado y por más que usáramos muchos comentarios no podríamos encontrar ni modificar porciones de código tan fácilmente como con los métodos. Un método es simplemente un bloque de código que se ejecuta cuando nosotros los programadores lo decidamos. Pensemos en nuestro ejemplo de un reproductor de audio. Cada vez que un usuario hace clic sobre un botón, por ejemplo el botón 'play', queremos siempre que un mismo código se ejecute, en este caso el código que hace que la canción suene. Sin los métodos sería imposible ejecutar solo una porción específica de código. Entonces no sólo usamos los métodos para organizar, sino que sin ellos es imposible crear aplicaciones grandes. Veamos el más simple de los métodos que podemos crear en una aplicación: public class MiClase { public static void main(String[ ] args) { MiClase miClase = new MiClase(); miClase.miMetodo(); } public void miMetodo() { System.out.println("Hola desde tu primer método"); } } 63 Crea un nuevo proyecto en NetBeans, nómbralo como quieras y crea como Main Class el nombre MiClase. Más simple que esto no podemos escribir un método así que vamos a analizarlo parte por parte. Antes de empezar trata de mirar qué tienen en común en su estructura los dos métodos que tenemos aquí, tanto el principal como el creado por nosotros en azul. Si comparamos ambos métodos, nos damos cuenta que los dos empiezan exactamente igual con la palabra public. Esta palabra es un modificador de acceso y es la encargada de permitir que desde clases externas a ésta puedan llegar a usar dicho método. Como veremos más adelante, es posible que nosotros queramos crear métodos a los cuáles solo pueda accederse desde dentro de la clase donde están creados y nunca desde fuera de ésta. Los métodos se protegen por razones que veremos más adelante en otro capítulo cuando veamos encapsulación. Lo importante es que entendamos que los métodos empiezan con una palabra que es un modificador de acceso y que cuando no nos importa que otras clases puedan acceder a este método usamos la palabra public, y cuando queremos protegerla ponemos la palabra private. Como todavía no sabemos crear otras clases podemos dejar esta discusión para más adelante. Si seguimos la comparación de nuestros dos métodos, vemos que el principal tiene una palabra static que nuestro método no tiene. Todo método main() es static, pero no todo método tiene que ser static. Nuevamente esta discusión la podremos hacer más adelante cuando veamos los objetos para que podamos entender más claramente a qué se refiere exactamente este modificador, mientras tanto es suficiente con que sepamos que todo método main() debe ser static y por ahora nuestros métodos no necesitan usar este modificador. Siguiendo con la comparación, encontramos que ambos métodos comparten la palabra void. Cuando llamamos un método desde cualquier parte, éste puede hacer una acción cualquiera, que es el código dentro de su bloque, y además puede devolver un resultado si así lo queremos. En el capítulo sobre matemáticas 64 usamos un método sin saberlo y fue Math.random() que es un método llamado random() dentro de una clase llamada Math, que trae Java ya escrita por nosotros. Cuando lo llamamos ocurre un bloque de código que desconocemos pero lo importante es que nos devuelve un número aleatorio entre o y casi 1. Así como en Math.random() muchas veces necesitamos que nuestros métodos devuelvan algún tipo de información. En nuestros dos métodos tenemos la palabra void que simplemente significa que NO vamos a devolver nada de estos métodos. Si queremos devolver algo de nuestros métodos, simplemente reemplazamos la palabra void por el tipo de información que vamos a devolver ya sea int, long, boolean, String, etc. Cuando queremos devolver información de un método simplemente ponemos el tipo de retorno al declarar el método como acabamos de ver y dentro del bloque escribimos el siguiente código: return variable; Escribimos la palabra return seguida por una variable que puede ser cualquiera, pero en este caso la hemos llamado variable, y esta información que devolvemos debe ser del mismo tipo declarado al comienzo del método. Más adelante veremos un proceso completo en el que usaremos un método que devuelva un resultado y luego haremos algo con ese resultado. Siguiendo con la comparación de nuestros dos métodos, después del tipo de retorno nos encontramos con el nombre de nuestro método. Este nombre debe usar CamelCase, debe empezar en minúscula y NO debe contener caracteres raros como tildes o signos de puntuación ni nada parecido. Después del nombre encontramos unos paréntesis (), en el método principal dentro del paréntesis dice String[] args y en nuestro método están vacíos. Estos paréntesis son obligatorios y aunque pueden estar vacíos, su función es declarar una variable que contenga cierta información específica que necesita el método para funcionar. Por ejemplo main() necesita recibir un arreglo del tipo String que por lo general se nombra args pero podemos poner el nombre que queramos. 65 Para entender bien lo que hacen estos paréntesis recordemos cuando aprendimos a ejecutar nuestro programa usando la línea de comandos, allí debíamos escribir java MiPrograma para poder ver lo que hacía nuestro código. Pues bien, después del nombre de nuestro programa también pudimos haber pasado parámetros al método main(). Parados dentro de la carpeta de nuestro archivo ya compilado, podemos escribir en la línea de comandos el siguiente código para ejecutar el programa y así enviar parámetros que permitan al método principal recibir argumentos: java MiPrograma cualquier cantidad de palabras En el caso anterior estamos pasando un arreglo con 4 casillas, cada una contiene una palabra, este arreglo tiene como contenido en su casilla args[0] la palabra cualquier, en la casilla args[1] tiene el texto cantidad, en la casilla args[2] está de, y en la casilla args[3] encontramos el texto palabras. Lo anterior es teniendo en cuenta que llamamos al arreglo de parámetros args. Podríamos por ejemplo hacer un programa en Java que nos saluda cuando lo ejecutamos. Probemos el siguiente código en NetBeans y aprendamos a pasar parámetros al método principal usando este programa. Creemos un nuevo proyecto con su Main Class llamada Saludo y escribamos el siguiente código: public class Saludo { public static void main(String[] args) { System.out.print("Hola "); for (int i = 0;i < args.length;i++) { System.out.print(args[i] + " "); } } } 66 Si compilamos nuestro código como hemos hecho siempre el resultado será Hola en la ventana de salida. Pero si ahora vamos a File > Project Properties, allí seleccionamos la categoría Run y en Arguments: escribimos nuestro nombre, luego hacemos clic en OK y volvemos a correr el programa, ahora podremos ver como resultado en la ventana de salida un saludo personalizado. Con este proceso quiero demostrarte la funcionalidad de los parámetros que pasamos a los métodos y la funcionalidad que tiene el arreglo de String que encontramos en el método principal. Cuando vimos el capítulo sobre la anatomía básica de Java, no podíamos entender todo lo que estaba escrito cuando declarábamos nuestro método main(), ahora ya entendemos que dentro de los paréntesis el método está creando un arreglo que es capaz de recibir texto para luego hacer algo con éste si queremos. Un mismo método puede producir resultados diferentes dependiendo de los argumentos que reciba. Si no lo necesitamos, podemos dejar los paréntesis vacíos y así el método no usará argumentos. Como conclusión, los paréntesis se usan para pasar información a un método. Terminando nuestra comparación del código que escribimos al comienzo de este capítulo, después de los paréntesis encontramos las llaves { } que encierran el bloque de código que se ejecutará con dicho método. Como repaso veamos que todo método empieza con un modificador de acceso, después la palabra static es opcional para nuestros métodos pero es obligatoria para el método principal, luego ponemos el tipo de retorno si queremos que el método devuelva algo y si no ponemos void, en seguida escribimos el nombre del método y después unos paréntesis en donde declaramos una variable que va a contener los parámetros que le pasemos al método si así lo queremos. Por último escribimos el bloque de código que queremos que corra. Con lo anterior podemos tener clara la estructura de un método pero todavía no sabemos cómo llamarlos para que se ejecute su contenido. Para entender cómo 67 hacer esto y mostrar un ejemplo en el que usemos argumentos y un retorno, voy a agregar otro método a nuestro código original. Si entendemos este nuevo código, entenderemos el código original y entenderemos cómo se relacionan los diferentes métodos dentro de una clase. public class MiClase { public static void main(String[ ] args) { MiClase miClase = new MiClase(); miClase.miMetodo(); } public void miMetodo() { System.out.println(mayus("Hola desde tu primer método")); } public String mayus(String texto) { String textoMayus = texto.toUpperCase(); return textoMayus; } } Si compilas este código verás que tenemos como resultado en la ventana de salida un texto en mayúsculas. Si bien todo el código anterior lo pudimos escribir de forma muy sencilla en una sola línea dentro del método principal, quiero que entiendas los procesos entre métodos tan importantes que están ocurriendo aquí ya que por lo general en aplicaciones reales que escribamos vamos a tener siempre este tipo de interacciones. De forma general lo que está ocurriendo es que cuando corremos la aplicación, Java ejecuta el código en el método principal, éste llama a un primer método creado por nosotros cuyo nombre es miMetodo() y que contiene un texto que le pasa a un segundo método creado por nosotros llamado mayus() que recibe el 68 texto y lo convierte todo en mayúsculas para luego devolverlo. El método llamado miMetodo usa el retorno de mayus() para imprimirlo en la ventana de salida. Si miras con detenimiento te darás cuenta que main() llama de forma diferente a miMetodo() comparado con la forma en que miMetodo() llama a mayus(). Esto ocurre por una razón muy simple y es porque el método main() es static. Un método static no puede llamar normalmente al resto de métodos dentro de su clase, la forma normal en que se llaman los métodos dentro de una clase cuando no son static es muy simple y es así: metodo(); Si el método requiere que le enviemos algo para funcionar entonces le escribimos el tipo de información correcta dentro del paréntesis. Observa que en miMetodo() usamos un System.out.println() en el que pusimos dentro del paréntesis una llamada al método mayus() con su respectivo String que necesita para funcionar. Como main() debe ser static entonces primero debemos crear un objeto de la clase en la que está contenido y luego si llamar dicho método a través del objeto. Esto puede sonar muy complicado pero en realidad no lo es, simplemente son conceptos que aprenderemos más adelante y por ahora podemos hacer un poco de acto de fe y simplemente creer en lo que digo y lo voy a repetir con palabras más simples: para llamar un método que está dentro de la misma clase desde el método principal, debemos crear una variable cuyo tipo va a ser el nombre de nuestra clase y la vamos a igualar a una nueva instancia del nombre de nuestra clase para luego usar la variable como punto de partida para llamar el método que necesitamos correr. Así como muestra el siguiente código: MiClase miClase = new MiClase(); miClase.miMetodo(); 69 Esta es la forma en que creamos objetos en Java. Por ahora no importa que no sepamos qué es un objeto, lo importante es que sepamos que los objetos se sacan a partir de las clases y que con el código anterior creamos una instancia de objeto de nuestra clase, le ponemos el nombre que queramos a la variable que contiene el objeto y que debe ser del tipo de nuestra clase, que es el mismo nombre de nuestra clase y luego usando el nombre de la variable con un punto y luego el nombre del método que queremos ejecutar, vamos a lograr poner a andar un método desde main(). En el código anterior usamos un método creado por Java y que hace parte de la clase String que nos permite convertir en mayúsculas un texto. Así como Math.random() que es un método llamado random() dentro de la clase Math, este método es como decir String.toUpperCase() solo que en vez de escribir String ponemos un texto entre comillas o una variable que contenga un String. El punto se usa en Java para decir que lo que sigue está contenido en lo anterior, en estos casos el método está contenido en la clase. Más adelante entenderemos y usaremos más claramente la sintaxis del punto. Como conclusión podemos entender que siempre que tengamos un método static, debemos crear un objeto de nuestra clase para poder llamar otros métodos. Cuando queramos llamar otros métodos desde un método que no es static simplemente escribimos su nombre seguido de los paréntesis con el argumento si es que lo necesitan. Mucho del contenido visto en este capítulo será aclarado cuando veamos objetos. Lo más importante es que no dejemos pasarlo sin entender que los métodos son bloques de código que pueden recibir información para manipularla o hacer algo con ella y devolver información si así lo queremos. Podríamos crear un método que genere ondas sinusoidales, puede recibir dos argumentos separándolos por comas que sean la frecuencia y la duración y este método podría devolver un arreglo con la información de la onda. 70 Ámbitos locales Una variable puede crearse fuera de los métodos así: public class Ambitos{ int numeroFueraDeMetodos = 123; public static void main(String[ ] args) { System.out.println(numeroFueraDeMetodos); } } En el código anterior hemos creado una variable fuera de un método. Al ponerla allí nos aseguramos que todo método que NO sea static pueda usarla. Cuando escribimos una variable dentro de un método es una variable local y por eso solo existe dentro del bloque del método, esto quiere decir que a las variables locales no pueden accederse desde fuera de su bloque. Muchas veces necesitamos que varios métodos compartan una misma variable y por eso la escribimos fuera de todo método. Sin embargo el código anterior NO compila. Esto ocurre porque estamos usando la variable dentro de un método static como lo es nuestro método main(). Como nos pasó en el capítulo anterior, cuando tenemos un método que es static tenemos que enfrentarnos a ciertos problemas, todos tienen solución. No me parece justo con los métodos static que hablemos solo de las cosas malas que nos traen ya que son muy útiles también. Si bien nos han traído problemas en el capítulo anterior y ahora aquí con las variables, primero que todo no podemos dejar de ponerle static al método principal y además los métodos estáticos son muy útiles ya que nos permiten acceder a ellos sin necesidad de crear objetos como tal, pero esto lo veremos más adelante. Por ejemplo todos los métodos dentro de la clase Math, la que nos permite hacer operaciones matemáticas y usar random(), son métodos static y esto nos permite acceder a ellos sin necesidad de crear un objeto de la clase Math. Esto no quiere decir que crear un objeto sea 71 malo, para nada, simplemente debemos tener claro que hay ocasiones en las que queremos acceder rápidamente a métodos sin necesidad de crear referencias a objetos como ya veremos más adelante y la única forma es volviéndolos static. Lo importante es tener en cuenta que cuando creamos una variable dentro de un método, ésta solo existe dentro del mismo. Si queremos que una variable exista para todos los métodos no estáticos podemos declararla fuera de los métodos. Si por alguna razón estamos desesperados por usar la variable dentro del método principal, podemos arreglar nuestro código anterior de la siguiente manera: public class Ambitos{ int numeroFueraDeMetodos = 123; public static void main(String[ ] args) { Ambitos ambitos = new Ambitos(); System.out.println(ambitos.numeroFueraDeMetodos); } } Estamos usando exactamente la misma solución del capítulo pasado que fue crear un objeto de nuestra clase para poder acceder a métodos o variables de nuestra clase desde el método main(). Otra opción es anteponer a la variable el modificador static y con esto ya podremos usarla. public class Ambitos{ static int numeroFueraDeMetodos = 123; public static void main(String[ ] args) { System.out.println(numeroFueraDeMetodos); } } Claro que la solución anterior tiene otras implicaciones para los objetos que examinaremos después. Las ventajas de static las veremos más adelante. 72 Podemos generalizar un poco más la teoría vista anteriormente de la siguiente forma: Un bloque define un ámbito. Cada vez que se inicia un nuevo bloque, se está creando un nuevo ámbito. Un ámbito determina qué objetos son visibles para otras partes del programa. También determina el tiempo de vida de esos objetos. (Schildt, 2009:42) Yo mismo caí en este error varias veces. Este es uno de los errores típicos por los que no entendemos por qué no compila un código. A veces creamos una variable, luego la vamos a usar en otra parte y simplemente no funciona, es como si la variable no existiera. Y de hecho es porque dicha variable no existe, pensemos que toda variable muere cuando se termina su ámbito, esto quiere decir cuando se cierra su bloque. Lo anterior nos lleva a concluir que cuando creamos una variable dentro del bloque de una sentencia de prueba if, ésta no existe fuera del bloque. Por ejemplo pensemos en el siguiente ejemplo: if (true) { int numero = 100; } System.out.println(numero); Este código no funciona porque simplemente System.out.println() se encuentra fuera del ámbito de la variable numero. Para solucionarlo debemos crear la variable fuera del bloque y modificarla dentro: int numero; if (true) { numero = 100; } System.out.println(numero); 73 Conversión de tipos Estamos en el último capítulo de la primera parte sobre Java, esto son muy buenas noticias porque quiere decir que nos estamos acercando al núcleo de este proyecto que es programar aplicaciones de audio. Sin embargo debo repetir que aunque todo lo visto hasta aquí no sea manejo digital de audio, todos estos conocimientos son necesarios para poder llegar a lo que más queremos. Si este proyecto de grado simplemente se saltara directamente al manejo del audio, probablemente nadie excepto los programadores en Java podrían entenderlo y una de las verdades claves es que son muy pocos los ingenieros de sonido que saben programar. Probablemente la persona que haya leído este proyecto hasta este punto tendrá muchas dudas y sentirá que hay explicaciones pasadas que quedaron incompletas. La verdad es que si te sientes así, es probablemente porque vas por muy buen camino ya que hasta aquí solo hemos dado unas nociones básica sobre el lenguaje. Java es un lenguaje puramente orientado a objetos, y como todavía no sabemos qué es un objeto pues todavía sabemos muy poco de Java. En la siguiente sección nos dedicaremos a aprender sobre los objetos y cómo podemos usarlos para crear excelentes aplicaciones de audio. Por ahora la clave es la paciencia. En el capítulo de matemáticas, descubrimos que cuando hacíamos divisiones de dos variables de tipo int, los resultados siempre se redondeaban al entero más cercano. La solución era usar un convertidor de tipos de la siguiente manera: (tipo) valor Donde (tipo) es simplemente el tipo al que se quiere convertir el valor o variable cuyo contenido es de otro tipo. Este proceso es conocido como cast. Imaginemos 74 que tenemos una variable de tipo int que queremos convertir al tipo byte. En este caso podemos usar un cast como muestra el siguiente código: public class Conversion { public static void main(String[] args) { int i = 1000; byte b = (byte) i; System.out.println(b); } } Sin embargo, al compilar y ejecutar el código anterior obtenemos -24 y no 1000 como esperábamos. Esto ocurre porque no podemos olvidar que una variable de tipo byte solo puede almacenar valores entre -128 y 127, por lo tanto estamos perdiendo bits de información útil y transformando el valor real. Debemos tener mucho cuidado siempre que usamos conversiones de tipos, porque el hecho de que nos permita compilar no quiere decir que la aplicación esté bien creada. Si en el ejemplo anterior la variable de tipo int fuera un número válido para caber en un byte entonces no habría problema. Debemos tener en cuenta dos posibles escenarios al convertir tipos. Primero cuando vamos a convertir de un tipo más grande a uno más pequeño, como en nuestro ejemplo pasado. En este caso podemos hacer la conversión sólo cuando estemos seguros que los valores caben dentro del tipo más pequeño. El segundo escenario es cuando tenemos un tipo más pequeño que queremos asignar a un tipo con mayor capacidad de almacenamiento, en este caso no es necesario usar un cast ya que Java convierte automáticamente por nosotros el tipo y no debemos preocuparnos por los valores porque siempre van a ser compatibles. Como conclusión, la conversión entre primitivos es muy fácil. Simplemente escribimos el tipo al que queremos convertir entre paréntesis, esto quiere decir 75 que hacemos un cast con el tipo deseado: (byte), (short), (int), etc. Seguido del cast escribimos el valor o variable que se encuentra en el tipo incorrecto. Un cast puede hacerse no sólo para los valores primitivos sino también entre objetos cuando sea posible. Aunque aún no hayamos visto objetos, puedo adelantarte que nosotros podemos crear objetos y su tipo es exactamente el mismo nombre de la clase que los contiene. Por ejemplo podemos tener una clase llamada MiClase que contiene la información para crear un objeto. Cuando sea necesario y sea posible, condiciones que veremos más adelante, podremos hacer cast entre objetos. Por ejemplo podremos escribir el siguiente código para convertir una variable llamada objeto que está en otro tipo, al tipo MiClase usando el siguiente cast y capturándolo en la variable correcta que en este caso he nombrado variable: variable = (MiClase) objeto; Con esto no quiero que aprendas sobre objetos todavía, sólo quiero desde ya aclarar que la sentencia cast permite usarse entre objetos, por lo tanto simplemente ponemos el tipo de objeto deseado entre paréntesis justo antes de la variable que contiene al objeto. Este proceso es exactamente igual a como hicimos con los primitivos. A veces tenemos un número dentro de una variable que queremos convertir a una cadena o String, esto quiere decir un número que queremos tratar como texto. Para esto podemos simplemente sumar el número a la cadena y el resultado es una cadena: String cadena = "23" + 23; 76 El código anterior no suma los números, simplemente los agrega a la cadena dando como resultado "2323". Si queremos por el contrario convertir un número de una cadena a un número entero podremos usar el siguiente código: String cadena = "23"; int entero = Integer.parseInt(cadena); En el caso anterior obtendremos como resultado que entero ahora carga el número 23. Integer.parseInt() es el código necesario para hacer esta conversión y dentro del paréntesis se agrega el texto que debe contener solo número y que se desea convertir. Si el texto que se pasa contiene letras vamos a obtener un error. La conversión entre tipos es un tema grande y para entenderlo del todo todavía necesitamos otros conocimientos como los objetos. De todas formas con las bases expuestas aquí podrás hacer las conversiones más comunes. Decidí explicar este tema de conversión de tipos antes de explicar objetos porque no quiero profundizar más en este tema para poder ir más rápido y enfocarme en la parte de audio. Si llegas a necesitar una conversión que no he enseñado aquí, simplemente busca en internet lo que estás tratando de convertir y hay muchas posibilidades que encuentres la forma correcta de hacerlo sin tener que buscar mucho. En conclusión, usamos la sentencia cast para permitir la conversión entre tipos. Cuando usamos primitivos sólo es necesario el cast cuando vamos a convertir de un tipo que use más bits a uno que use menos, pero debemos ser cuidadosos para que el valor quepa en el tipo más pequeño. La sentencia cast también se usa entre objetos y aunque no sepamos todavía sobre objetos, sabemos que se puede poner entre paréntesis el tipo deseado y así podremos convertirlos, pero esto solo puede pasar bajo ciertas condiciones que veremos más adelante. Si lo deseamos también podemos pasar de números primitivos a cadenas sumando una cadena con el número. Para lo contrario podemos usar Integer.parseInt(). 77 ¿Qué son los objetos? Hasta ahora hemos nombrado mucho los objetos pero hemos aprendido poco sobre ellos. De hecho Java es un lenguaje OOP por sus siglas en inglés Object Oriented Programming o en español POO Programación Orientada a Objetos. Esto implica que todo el lenguaje se estructura a partir de objetos y es prácticamente imposible usarlo y pensarlo sin entender el mundo OOP. ¿Qué son los objetos? Un objeto en Java puede pensarse como un objeto de la vida real. Volviendo al ejemplo de un reproductor de audio, pensemos en uno de los objetos más famosos de nuestro tiempo, un IPOD. Ya no pensemos que estamos creando en Java un simple reproductor de audio, pensemos que estamos creando un IPOD virtual. Este IPOD podría verse en pantalla exactamente igual a uno físico, además tendría los mismos botones y su pantalla y funciones serían las mismas. Crear este tipo de aplicación es perfectamente posible en Java. En este proyecto de grado no voy a crear un IPOD por ustedes, en cambio voy a hacer referencia a éste para tener clara la noción de objeto y seguiré dando explicaciones sobre el lenguaje basándome en este famoso objeto para que ustedes si así lo desean tengan la capacidad de crearlo desde sus casas sin que yo les dé el código completo. En la primera sección ya vimos mucho del lenguaje que nos va a servir para crear un IPOD virtual. Pensemos por ejemplo lo útil que puede ser Math.random() para poder oír canciones de forma aleatoria. La programación orientada a objetos está pensada para nosotros los programadores y no exactamente para el usuario final. Esto quiere decir que podemos crear aplicaciones orientadas a objetos, o no, y el resultado puede lograrse igual para que la aplicación funcione. El punto es que cuando usamos una estructura de objetos vamos a poder tener códigos más claros, vamos a poder mantener mejor nuestro código en el futuro y vamos a poder crear varios objetos partiendo de un mismo código. 78 Por ejemplo, si creamos nuestro IPOD pensando en objetos, podremos crear en pantalla muchos IPOD diferentes al tiempo, con muy pocas líneas de código. Sería raro que quisiéramos varios reproductores de música abiertos al mismo tiempo, pero pensemos lo útil que puede llegar a ser si en vez de crear un IPOD estuviéramos creando una consola de 64 canales. En este caso podríamos hacer un objeto que fuera un ChannelStrip y no tenemos que repetir nuestro código inútilmente 64 veces, simplemente partiendo del mismo código hacemos 64 objetos de ChannelStrip y hemos terminado. Pero lo mejor de todo es que si queremos agregarle una función extra a todos nuestros canales de la consola, no tenemos que modificar 64 códigos diferentes, simplemente modificamos el código del objeto ChannelStrip, volvemos a compilar y a ejecutar nuestro programa y automáticamente se actualizan los 64 canales con la nueva función que hayamos creado. Empecemos por el final de la historia, imaginemos que ya terminamos todo nuestro código que nos permite crear un IPOD. Supongamos que este código está dentro de una clase llamada IPod. Las clases no son objetos, pero si son un contenedor para escribir todo el código que necesita un objeto. Podemos pensar las clases como los planos y los materiales de una casa, esto significa que tienen el potencial de ser un objeto llamado casa, pero sólo se convierten en casa hasta que usamos y unimos correctamente sus partes. De la misma forma la clase no es objeto hasta que no lo declaremos, más adelante veremos cómo hacer esto. Cuando vamos a comprar un IPOD real nos hacen tres preguntas: qué modelo, qué capacidad de almacenamiento y qué color. En el capítulo sobre métodos aprendimos que podíamos pasarle uno o varios parámetros a un método para que este reaccionara diferente de acuerdo con la información que le llega. De la misma forma, podemos crear objetos que necesitan argumentos para poder ser creados. En este caso vamos a crear un objeto de la clase IPod que necesita saber tres argumentos para poder crear un nuevo objeto: el modelo, la capacidad y el color. 79 El siguiente sería el código que pondríamos en nuestro método principal para crear un nuevo IPod nano de 8GB y de color azul. IPod myIPod = new IPod("nano", 8, "azul"); Con el código anterior hemos creado un objeto de la clase IPod y que hemos guardado en una variable llamada myIPod. Ésta se llama variable de referencia al objeto ya que es una representación del objeto, la usamos para luego llamar métodos para este IPOD específico. Recordemos que cuando queríamos llamar otros métodos credos por nosotros desde el método principal, como es un método static, no podíamos llamarlos directamente, nos veíamos en la obligación de crear un objeto de nuestra clase para llamar métodos no estáticos desde la variable de referencia de nuestra clase: MiClase nombreReferencia = new MiClase(); nombreReferencia.miMetodo(); En el código anterior no le pasamos parámetros a la clase ya que ésta puede no recibir argumentos, depende de cómo esté creada nuestra clase. Más adelante veremos cómo trabajar con argumentos para las clases. También usamos la variable nombreReferencia, que es la variable de referencia a nuestro objeto de nuestra clase para poder llamar al método miMetodo() usando un punto entre ellos. Como podemos ver, tanto el código que crea el IPOD como el que crea un objeto de nuestra clase es exactamente igual y sólo se diferencian porque uno trabaja con argumentos y el otro no. De resto son iguales: primero declaran el tipo de objeto, luego se escribe el nombre de la variable de referencia, luego se iguala a una nueva instancia del tipo de clase con sus paréntesis para poder pasar parámetros y luego termina en punto y coma. La palabra clave new especifica que se está creando una nueva instancia del objeto que se escribe a continuación. 80 Sobre la variable de referencia de nuestro IPOD myIPod, también podemos llamar métodos que hayamos creado dentro de la clase IPod. Dicho método debe tener su modificador de acceso como public o de lo contrario no vamos a poder usarlo. Por ejemplo pudimos haber credo un método que nos permite prender el IPOD y que llamamos prender(). En este caso prenderíamos nuestro IPOD con el siguiente código: myIPod.prender(); Para este código no necesitamos escribir argumentos ya que prender es igual en todos los casos imaginables de IPOD. Con el siguiente código podríamos crear en pantalla dos IPOD diferentes y cada uno se controlaría desde el código con su respectiva variable de referencia. IPod miNano = new IPod("nano", 16, "negro"); IPod miTouch = new IPod("touch", 32); miNano.prender(); miTouch.prender(); En el código anterior hemos creado dos IPOD independientes, el primero es un IPOD nano de 16 Gigas y de color negro. El segundo es un IPOD touch de 32 Gigas y en este caso no le pasamos información sobre el color porque este modelo de IPOD sólo viene en negro. Con lo anterior quiero demostrar que es posible escribir una misma clase que pueda aceptar listas diferentes de argumentos para funcionar. Más adelante veremos cómo se logra esto desde el código de la clase. Por último prendimos cada uno de los IPOD desde su respectiva variable de referencia. Ya sabemos que una clase es el contenedor necesario para escribir el código para crear objetos. Sin importar cuantas clases tengamos, una de ellas debe tener un método main() que es el encargado de inicializar todo nuestra aplicación. Por 81 ahora vamos a crear los objetos desde el método principal. A la hora de crear varias clases para un mismo proyecto podemos escribirlas en un mismo archivo .java, o si preferimos podemos crear un archivo .java aparte del que contiene main() para cada clase. Empecemos por la forma más rápida que es crear diferentes clases dentro de un mismo archivo. Hasta ahora para crear las clases hemos escrito public class Nombre y luego el bloque. Cuando creamos clases y las nombramos public, deben estar dentro de un archivo con su mismo nombre. Es por esto que no podemos crear más de una clase como public dentro de un mismo archivo .java. Cuando ponemos varias clases dentro de un mismo archivo, sólo la que se llame como el archivo puede ser public. public class Main { public static void main(String[] args) { IPod miIpod = new IPod("nano", 8); miIpod.prender(); } } class IPod { public IPod(String modelo, int capacidad) { System.out.println("Compraste un nuevo IPOD " + modelo + " de " + capacidad + " Gigas."); } public void prender() { System.out.println("IPOD prendido."); } } En el código anterior supongamos que estamos creando una tienda de IPOD. El código para comprar nuevos IPOD va todo en la clase Main. Compila y ejecuta el código anterior y mira el resultado. Todo el código anterior puede ir en un solo archivo que debe llamarse Main.java ya que esta clase es public y es la que tiene 82 main(). Si tratas de ponerle public a la clase IPod, el código no compilará porque sólo una clase puede ser public dentro de un mismo archivo. En el código simplemente tenemos dos clases: Main y IPod. La primera es la que tiene el método principal que crea un nuevo IPOD nano de 8 Gigas, por simplicidad omitimos el color. Observa que estamos pasando dos argumentos al objeto separándolos con coma. La segunda clase tiene un método que se llama constructor por tener el mismo nombre de la clase, esto quiere decir que es el método encargado de ejecutarse automáticamente cada vez que se crea un nuevo objeto de su clase. Este método no puede especificar el tipo de retorno porque no puede devolver nada. Este constructor recibe dos argumentos, observa que los creamos del tipo correcto y luego les dimos un nombre significativo para usarlos dentro del bloque. En el capítulo de métodos no vimos cómo pasar más de un parámetro ni cómo recibirlos, esta es la forma correcta de hacerlo, simplemente se separan por comas. El constructor es el encargado de recibir los argumentos que escribimos dentro del paréntesis cuando creamos una nueva instancia de un objeto. Dentro de la clase IPod también creamos un método llamado prender() que sería el encargado de cargar todo el código para encender el aparato. Podemos pensar los objetos como cheques de bancos. Cada cheque tiene una misma forma y básicamente todos sirven para lo mismo, pero el contenido de cada uno puede ser muy diferente y sobre todo, cada cheque es totalmente independiente del otro. Entonces si hacemos varias instancias de la clase IPod, cada una es totalmente independiente de la otra, si prendemos uno, solo ese se encenderá. Aquí hemos dado hasta ahora un abrebocas de lo que son los objetos, pero en realidad son mucho más poderosos. Existen tres principios que gobiernan la programación orientada a objetos y hasta ahora no hemos visto ninguno así que para programar realmente pensando en objetos debemos entender la encapsulación, la herencia y el polimorfismo. 83 Encapsulación Cuando tenemos un IPOD, este aparato tiene muy pocos botones, ellos de forma fácil nos permiten acceder y modificar el contenido. Estos botones existen no sólo para facilitarnos el funcionamiento del IPOD sino también para proteger los posibles errores que pudiéramos cometer si manejáramos directamente los circuitos del aparato. Si lo pensamos bien, cuando presionamos un botón, están ocurriendo muchas funciones internas que desconocemos, pero este botón protege este funcionamiento para que sea el correcto. Si como usuarios debiéramos saber el funcionamiento interno para poder manipular un IPOD, probablemente nadie tendría uno. Este proceso de proteger alguna labor interna encerrándola en un botón es un ejemplo de encapsulación, que es uno de los principios básicos de la programación orientada a objetos. La encapsulación no es más que una cantidad de procesos que están ocurriendo internamente, pero que nosotros como programadores vamos a proteger para que otra persona que use nuestros códigos pueda manejar correctamente, incluso para que nosotros mismos los usemos de forma debida. Pensemos que cuando creamos un objeto cuya función va a ser reproducir audio, podemos reutilizar este código tan genérico en muchas aplicaciones, incluso en proyectos grandes otros programadores podrían llegar a usarlos. Crear un objeto en Java que nos permita reproducir audio con una sola línea de código sería un sueño, porque como veremos más adelante, reproducir audio en Java requiere varios conocimientos y varias líneas de código. Sin embargo, gracias a los objetos y a la encapsulación, podríamos proteger todo el código complejo para luego simplemente usar dicho objeto para reproducir audio de forma sencilla y libre de errores, esto quiere decir que la encapsulación va a permitir que encerremos lo complejo y lo mantengamos protegido para siempre poder crear una forma fácil de usar dicho código. Siempre que tengamos una nueva aplicación que necesite audio, sacamos nuestro objeto para reproducir audio y estamos listos para agregar audio en nuestra aplicación con tan sólo unas líneas de código. Si por ejemplo hay más programadores 84 involucrados en dicha aplicación, nosotros somos los encargados del audio y ellos del montaje final, les enseñamos a usar nuestro objeto como aplicación y ellos nunca tendrán que modificarlo, solo tendrán que aprender a usarlo, así nosotros nos aseguramos que manejen bien el audio, protegiendo las aplicaciones y nuestro objeto de los posibles errores que ellos pudieran cometer. La mejor forma de entender la encapsulación es usarla. Usemos la encapsulación de forma básica. Vamos a crear nuestro objeto IPod que necesita un método que se llama siguienteCancion() y como su nombre lo indica es el encargado de pasar a la siguiente canción. public class Main { public static void main(String[] args) { String[] canciones = {"Canción 1", "Canción 2", "Canción 3", "Canción 4"}; int cancionActual = 0; System.out.println("Canción actual: " + canciones[cancionActual]); IPod miIpod = new IPod(); cancionActual = miIpod.siguienteCancion(cancionActual, canciones); } } class IPod { public int siguienteCancion(int actual, String[] lista){ System.out.println("Canción actual: " + lista[actual + 1]); return actual + 1; } } En este caso estamos creando una clase para el objeto IPod que no tiene método constructor ya que no es obligatorio crear uno. Para esta aplicación tenemos una lista de canciones que hemos declarado en el arreglo canciones. En la variable llamada cancionActual tenemos el número de casilla del arreglo o canción que está sonando en este momento. Luego usamos el objeto IPod y su método 85 siguienteCancion() que necesita saber la canción actual y recibe un arreglo de las canciones disponibles para buscar la siguiente canción en la lista y devolver el número para actualizar cancionActual. Sin embargo, si ponemos como canción actual la número 3 vamos a obtener un error en el código porque sobrepasamos las casillas del arreglo. En este caso hemos encontrado un error, para solucionarlo nos aprovechamos de la encapsulación que nos ofrece el método del objeto, esto quiere decir que desde main() seguiremos usando el mismo código pero gracias a que el verdadero código que cambia la canción está encapsulado en el método llamado siguienteCancion(), podemos arreglar el problema allí, sin modificar el código donde usamos el objeto que es la clase Main. class IPod { public int siguienteCancion(int actual, String[] lista){ if(actual == lista.length - 1) { System.out.println("Canción actual: " + lista[0]); return 0; } else { System.out.println("Canción actual: " + lista[actual + 1]); return actual + 1; } } } En el código anterior mostramos sólo la clase IPod porque sólo necesitamos este cambio para arreglar el error. Si hubiésemos escrito el código que nos permite cambiar de canción directamente en Main, tendríamos que modificar nuestro código allí para solucionar errores y eso no es protección, eso es todo lo contrario a la encapsulación que nos ofrece la programación orientada a objetos. Recordemos que creamos objetos para poderlos reusar. Si hubiésemos usado nuestro objeto IPod en muchas aplicaciones, con sólo modificar directamente el objeto y volver a compilar las aplicaciones ya tendríamos solucionado el problema, 86 en cambio si hubiésemos creado el código directamente en cada aplicación, nos tocaría modificar el código en cada una de las aplicaciones y volver a compilarlas. Con esta modificación en el código del objeto, ahora podemos poner en main() que cancionActual es igual a 3 y vamos a ver que la siguiente canción va a ser la casilla 0, eso quiere decir que hemos eliminado el problema desde la encapsulación. También podemos probar con cualquier número del 0 al 3 y todos van a funcionar. Pero ahora hemos llegado a otro problema y es que cancionActual es una variable que alguien podría igualar a 5 ó cualquier número fuera del índice de casillas del arreglo, si intentamos esto en nuestro código vamos a obtener un error al compilar, así que lo mejor es modificar nuestro código para que esa variable sólo exista dentro del objeto y no pueda ser modificada. En este caso no voy a aprovechar la encapsulación ya que voy a modificar el código en main() y el método siguienteCancion() ahora solo acepta un argumento. Hago esto para limpiar el código anterior y mostrar otro ejemplo de encapsulación más claramente. Pensemos que este es otro código posible para nuestro IPod y que aunque en este caso no estamos aprovechando la encapsulación para solucionar errores, quiero partir de este ejemplo diferente para mostrar otro punto importante. public class Main { public static void main(String[] args) { String[] canciones = {"Canción 1", "Canción 2", "Canción 3", "Canción 4"}; int cancionActual; IPod miIpod = new IPod(); cancionActual = miIpod.siguienteCancion(canciones); cancionActual = miIpod.siguienteCancion(canciones); cancionActual = miIpod.siguienteCancion(canciones); cancionActual = miIpod.siguienteCancion(canciones); cancionActual = miIpod.siguienteCancion(canciones); 87 } } class IPod { int estaCancion = 0; public int siguienteCancion(String[] lista){ if(estaCancion == lista.length - 1) { estaCancion = 0; System.out.println("Canción actual: " + lista[0]); return 0; } else { estaCancion ++; System.out.println("Canción actual: " + lista[estaCancion]); return estaCancion; } } } El código anterior agrega una variable llamada estaCancion dentro de la clase IPod. Esta variable es la que reemplaza cancionActual que teníamos antes en main(). Al hacer esto ya sólo necesitamos que siguienteCancion() pida un argumento que es la lista de canciones. Con esto buscamos proteger nuestro código para que nadie pida casillas del arreglo que no existen. En main() pedimos varias veces siguienteCancion() sobre nuestra variable de referencia al objeto para que veamos en la ventana de salida que siempre nos mantenemos dentro de nuestra lista de canciones que podemos modificar a nuestro gusto y el código siempre va a funcionar así sea una lista de 4 ó 10000 canciones. Sin embargo todavía no estamos utilizando la encapsulación a nuestro favor. Así como podemos llamar métodos de un objeto desde nuestra variable de referencia, también podemos llamar y modificar variables del mismo desde fuera. Eso quiere 88 decir que aún no hemos protegido nuestro código, todavía puede llegar alguien y decir desde main() que nuestra variable que creíamos protegida llamada estaCancion es igual a algo indeseable como una canción fuera del arreglo. Esto sería una vulnerabilidad en nuestra seguridad, alguien simplemente podría poner el siguiente código en medio de un llamado a siguienteCancion(): miIpod.estaCancion = 5; Este código se parece a la forma en que llamamos métodos desde nuestra variable de referencia, aquí la estamos usando para modificar una de sus variables. Como el índice 5 no es válido para nuestro arreglo vamos a obtener un error al ejecutar nuestro programa al momento en que llamamos el método usando este índice. Antes de continuar repasemos la forma en que se llaman métodos de un objeto: miReferencia.metodo(); Si el método devuelve algún valor podemos capturarlo de la siguiente forma: variable = miReferencia.metodo(); Si queremos modificar una variable permitida dentro del ámbito de la clase podemos hacerlo así: miReferencia.variableObjeto = 3; Podemos capturar el valor de una variable de un objeto así: variable = miReferencia.variableObjeto; 89 Después de este repaso volvamos a nuestro código. El punto fundamental con el código de nuestro IPod es que no podemos permitir que nadie modifique la variable estaCancion y para eso usamos los ya nombrados modificadores de acceso. Recordemos que hemos mencionado que al crear un método e incluso con las clases poníamos la palabra public para que porciones de código externas pudieran acceder a éstos. Resulta que las variables también pueden tener modificadores de acceso, de hecho cuando creamos una variable y no le especificamos un modificador de acceso, por defecto se convierten en default que es un nivel de acceso muy parecido a public. Esto quiere decir que estas dos líneas de códigos son muy parecidas para el código que tenemos. int estaCancion = 0; public int estaCancion = 0; Mira dónde se especifica el modificador de acceso, justo antes del tipo de variable. En este caso lo que necesitamos es cambiar la palabra public por la palabra private para tener el siguiente código dentro de la clase IPod: private int estaCancion = 0; Lo que quiere decir realmente la palabra private es que esta variable no puede ser modificada desde fuera de la clase y es exactamente eso lo que estamos buscando, proteger nuestro código de errores al usar nuestro objeto, esto quiere decir encapsulación. En realidad cuando no escribimos un modificador de acceso, lo que obtenemos es default que aunque es muy parecido a public no son exactamente lo mismo. public significa que cualquier código externo puede acceder al método, variable, clase o constructor que no haya declarado explícitamente su nivel de acceso. default significa que todo código que esté en el mismo package o paquete de clases que veremos más adelante, podrá acceder. En este caso no hemos declarado paquetes aún pero más adelante cuando los veamos podremos ver que sólo las clases que estén en el mismo paquete pueden 90 acceder al código marcado como default que es cuando no especificamos un modificador de acceso. Tenemos un último modificador de acceso llamado protected que funciona como default pero también permite a las sub-clases acceder al método, variable o clase que lo tiene así estén fuera del paquete. En el capítulo sobre herencia entenderemos qué son las sub-clases, en todo caso por lo general sólo usamos public o private así que por ahora no tenemos que preocuparnos por los otros tipos de modificadores. Por último en nuestro aprendizaje sobre encapsulación, recibamos los consejos de los grandes programadores en Java: Here’s an encapsulation starter rule of thumb (...): mark your instance variables private and provide public getters and setters for access control. When you have more design and coding savvy in Java, you will probably do things a little differently, but for now, this approach will keep you safe. (Bates y Sierra. 2005:81) Para explicar de forma correcta esta cita, primero debemos dejar claro que toda variable que sirva para crear y mantener un objeto la llamaremos variable de referencia, y todo el resto de variables como las que guardan tipos primitivos las llamaremos variables de instancia. En este caso nos recomiendan que usemos siempre private para todas las variables de instancia de un objeto, y que marquemos como public los getters y setters. Los getters y setter no son más que métodos que usamos para encapsular variables de instancia para poder validar información de ser necesario y poder trabajar con variables de instancia de forma correcta. Imaginemos que en nuestro IPod si queremos permitir que desde main() se pueda modificar la variable estaCancion pero no directamente sino a través de un método que lo haga correctamente, por si alguien intenta poner datos incorrectos, no pueda hacerlo. Simplemente los setters son métodos encargados de cambiar el valor de una variable de forma segura y por convención los llamamos empezando con la palabra set. En nuestro ejemplo podríamos crear un método dentro de la clase 91 IPod llamado setCancion() que se va a encargar de recibir el número que se quiera de canción y la lista de canciones, pero antes va a averiguar si se está pidiendo una canción correcta: public boolean setCancion(int numeroCancion, String[] lista) { if(numeroCancion < lista.length) { estaCancion = numeroCancion; return true; } else { return false; } } En este caso tenemos un método simple que devuelve true si se puede poner ese número de casilla del arreglo canciones, si el valor no es permitido entonces devuelve false. Este método es un setter porque valida la información para manipular una variable de instancia. Un getter es lo mismo pero se usa para obtener el valor de una variable de instancia y no para modificarla, por convención se nombran empezando con la palabra get: public int getCancion() { return estaCancion; } Este getter devuelve el valor de estaCancion. Sin el getter no podríamos acceder a la variable de instancia porque está marcada como private. Además si más adelante se nos ocurre hacer una validación podemos poner el código dentro de este bloque y no dañamos el código de main(). 92 Herencia En estos últimos capítulos hemos creado una clase llamada IPod que hemos usado para cualquiera de los diferentes modelos de IPOD existentes. Cuando empecé a hablar sobre objetos dije que podíamos pasarle al constructor un argumento que especificara el modelo. Los diferentes modelos de IPOD guardan muchos elementos en común, al fin y al cabo todos son IPOD, pero a la hora de la verdad hay diferencias importantes entre unos y otros. Por un lado está el tamaño, por otro lado está como se ven visualmente, su interfaz no es exactamente la misma así se parezcan, etc. Es por esto que si vamos a crear una sola clase que maneje todos los tipos de IPOD, vamos a necesitar escribir mucho código independiente para cada modelo en un mismo bloque o método, lo que nos llevará a usar muchas sentencias de control. Por razones de organización en el código y para poder manejar de forma fácil todos los futuros modelos de IPOD que puedan salir al mercado, la anterior no parece una buena solución. ¿Cuál es entonces la mejor forma de hacerlo? Herencia al rescate. La herencia es la posibilidad que nos brinda la programación orientada a objetos, para poder crear subclases. Una subclase es una clase que hereda todas las variables de instancia y métodos públicos o protegidos de una clase madre, pero además puede tener sus comportamientos propios e incluso puede modificar los comportamientos heredados. En nuestro ejemplo, la mejor solución es crear una clase que se llame IPod que contenga todas las características que tienen en común todos los modelos de IPOD, luego creamos subclases de la clase IPod que sirvan para crear específicamente cada modelo, la ventaja es que al ser subclases heredan inmediatamente los comportamientos y no tenemos que volver a escribirlos para cada modelo, en estas subclases solo debemos escribir lo particular de cada modelo. La siguiente es la estructura básica de una herencia en Java. Observa que vamos a crear una subclase de IPod llamada Nano. Al crear un objeto de esta subclase 93 podemos llamar el método prender() que es de su clase madre y no de ella misma, esto quiere decir que Nano ha heredado el método prender(): public class Main { public static void main(String[] args) { Nano nano = new Nano(); nano.prender(); } } class IPod { public void prender() { System.out.println("IPOD encendido."); } } class Nano extends IPod { } Para heredar una clase simplemente escribimos después del nombre la palabra extends seguida del nombre de la clase madre. En este caso observa cómo hacemos que Nano herede el comportamiento de IPod, esto quiere decir que Nano es una subclase de IPod, por lo tanto IPod es una superclase de Nano. Desde main() estamos creando un nuevo objeto de Nano y sobre éste estamos llamando su método heredado prender(). Hay muchas más posibilidades que nos brinda la herencia. Al día de hoy que escribo este proyecto de grado, el IPOD Shuffle no tiene pantalla pero todos los demás modelos si tienen. Como la mayoría de IPOD poseen una pantalla, sería muy bueno crear un método en la superclase que permitiera crearla, pero ¿qué podemos hacer con la subclase Shuffle que no tiene pantalla? En este caso 94 podemos sobrescribir un método de la clase madre para que se comporte diferente en la subclase Shuffle. public class Main { public static void main(String[] args) { Shuffle shuffle = new Shuffle(); shuffle.crearPantalla(); } } class IPod { public void crearPantalla() { System.out.println("Pantalla creada."); } } class Shuffle extends IPod{ public void crearPantalla() { System.out.println("No me puedes crear una pantalla."); } } Lo que hemos hecho es crear un método crearPantalla() para la clase IPod pero lo hemos sobrescrito en la subclase Shuffle. Para este ejemplo sencillo, como sólo hay un método y lo estamos sobrescribiendo pues es como si nunca hubiéramos heredado nada, pero al hacer todo el código necesario para crear los diferentes IPOD es muy posible que encontremos situaciones donde no queremos heredar un método particular para una subclase específica. Si lo quisiéramos también podríamos agregar cierto comportamiento a un método heredado sin borrar el comportamiento original del método. Por ejemplo podemos agregar un método llamado setColor(), recordando los setters, en el que podemos 95 crear el color del IPOD. Por cada Nano rojo que compremos, apple dona un porcentaje para las personas con SIDA en África, entonces en este caso necesitamos que cuando creamos un Nano color rojo, se cree el color normalmente, esto quiere decir que se llame el método setColor() de la superclase, pero además necesitamos que ejecute un código particular diferente a los demás modelos de IPOD: public class Main { public static void main(String[] args) { Nano nano = new Nano(); nano.setColor("rojo"); } } class IPod { public void setColor(String color) { System.out.println("El color de tu IPOD es: " + color); } } class Nano extends IPod{ public void setColor(String color) { super.setColor(color); if(color.equals("rojo")) { System.out.println("Hemos hecho una donación a África."); } } } En este caso hemos sobrescrito el método setColor() pero además le agregamos dentro de su bloque el código super.setColor() que significa ejecuta setColor() tal y como se encuentra en la superclase. Sin este código simplemente hubiésemos 96 sobrescrito del todo el método que significa ignorar su comportamiento original. La palabra super sirve para hacer referencia a la superclase. Dentro del método sobrescrito también hemos agregado el código que hace una donación cuando el color sea rojo. El libro Head First Java (Bates y Sierra. 2005:177) hace una recomendación que he encontrado muy útil para saber cuándo debemos crear una subclase y cuándo no. La propuesta encontrada en el libro es usar la prueba 'es un(a)' o 'tiene un(a)'. Por ejemplo si queremos saber si Nano debe ser una subclase de IPod, entonces nos preguntamos: ¿Nano es un IPod? Si la respuesta a una pregunta 'es un(a)' da positivo entonces es muy probable que debamos proceder creando una subclase. Cuando la pregunta nos da negativo debemos preguntarnos usando 'tiene un(a)' para nuestro ejemplo sería ¿Nano tiene un IPod?, lo cual suena totalmente ilógico. Cuando esta segunda prueba usando 'tiene un(a)' da positivo, entonces es muy probable que Nano deba ser una variable de instancia dentro de la clase IPod en vez de una subclase. Observemos que en el código anterior cuando sobrescribimos el método setColor(), éste recibe argumentos, cuando sobrescribimos un método, éste debe recibir exactamente los mismos argumentos que el método original. Recordemos que los métodos también pueden devolver valores, todo método sobrescrito debe devolver el mismo tipo de valor que el método original. Debemos tener cuidado porque las variables de instancia también se heredan, pero recordemos que normalmente debemos marcar estas variables como private, y todo lo que tenga private NO se hereda. Antes sobrescribimos un método, pero también podemos sobrecargar un método. Sobrecargar un método se usa cuando necesitamos una lista diferente de argumentos para correr un mismo método. Sobrecargar métodos no tiene nada que ver directamente con la herencia, pero ya que estamos hablando de sobrescribir métodos debemos aprender a diferenciar sobrescribir de sobrecargar 97 un método. Recordemos que cuando vamos a crear un nuevo IPOD, es buena idea escribir el color al momento de la creación del objeto, pero imaginemos que queremos darle la posibilidad a una persona que crea un nuevo Nano, que lo cree sin especificar el color y cuando esto pase se cree por defecto uno rojo: public class Main { public static void main(String[] args) { Nano nano1 = new Nano("azul"); Nano nano2 = new Nano(); } } class Nano{ public Nano(String color) { System.out.println("Has creado un nuevo NANO color: " + color); } public Nano() { System.out.println("Has creado un nuevo NANO color: rojo"); } } En este caso estamos creando dos objetos diferentes de la misma clase Nano. En el primero estamos especificando el color y en el segundo dejamos que el programa escoja por nosotros. Como puedes ver, para poder hacer esto debemos sobrecargar el constructor, simplemente lo volvemos a escribir como puedes ver en el código dentro de la clase Nano, la condición es que su lista de argumentos sea diferente. Esto nos permite mayor flexibilidad a la hora de usar un método cualquiera o un constructor. En este caso, el ejemplo es con un constructor pero también se puede hacer con métodos normales. 98 Gracias a la herencia, ya no queremos que se puedan hacer objetos directamente sobre la clase IPod porque ésta existe como madre de los diferentes tipos de IPOD pero no sirve para hacer un IPOD directamente. Pensemos que cuando tengamos nuestro código terminado, al crear un nuevo objeto de Nano ya sabremos lo que veremos, al crear un objeto de Touch ya sabremos el resultado, pero al crear un objeto de IPod no tenemos idea qué veremos ya que es una clase abstracta, no está hecha para crear objetos de ella misma sino de sus subclases. Para evitar que de una clase se creen objetos la marcamos abstract: abstract class IPod { // Todo el código de la clase IPod } Toda clase abstracta debe ser extendida, esto quiere decir que debe tener subclases. Los métodos también pueden ser abstractos y éstos deben ser sobrescritos. Un método abstracto no tiene cuerpo, esto quiere decir que no tiene llaves { }, no tiene bloque de código y se usa como recordatorio de algo que deben hacer las subclases. Por ejemplo sabemos que todos los IPOD tienen una capacidad en gigas diferente entre ellos, por lo tanto sería buena idea crear un método abstracto en la clase IPod, que sirve como recordatorio para las subclases y que obliga a todas ellas a sobrescribir el método encargado de asignar una capacidad al IPOD. Entonces una posible idea sería crear el siguiente método abstracto en la clase IPod: public abstract short setCapacidad(); Como podemos ver hemos creado un método abstracto que obliga a todas las subclases a sobrescribir este método y por lo tanto las obliga a cuadrar la capacidad correctamente para cada modelo. Le hemos puesto short como tipo de retorno porque sería bueno que este método devolviera un número indicando la capacidad de gigas. Cuando marcamos un método como abstracto, es obligatorio 99 marcar la clase también como abstracta. Un método que estaba marcado como abstracto, al sobrescribirlo e implementarlo se denomina método concreto aunque no hay que escribirle nada especial, simplemente lo sobrescribimos como aprendimos antes. A las clases que extienden una clase abstracta también las denominamos concretas si no tienen la palabra abstract. Hay veces en las que queremos extender más de una clase al tiempo. Por ejemplo pensemos en un IPHONE, para crearlo deberíamos extender IPod porque tienen muchas cosas en común, pero si tuviéramos una clase llamada Phone, también quisiéramos extenderla. En este caso no tenemos opciones en cuanto a heredar las dos porque Java no permite el heredamiento múltiple. Lo único que podemos hacer es crear una interfaz. Una interfaz es una clase con todos sus métodos abstractos, ninguno tiene cuerpo, todos son recordatorios. Para crear una interfaz: public interface Phone { // Métodos abstractos, todos son public y abstract. } Notemos que escribimos interface en vez de class. Para implementar la interfaz Phone y extender IPod para la clase IPhone procedemos así: public class IPhone extends IPod implements Phone { // Código de IPhone } Podemos implementar varias interfaces separándolas por comas. En resumen la herencia es esencial en la programación orientada a objetos. Con la palabra extends hacemos una subclase. Podemos sobrescribir y sobrecargar métodos, ambos son diferentes. También podemos escribir clases y métodos abstractos que deben ser extendidos y sobrescritos respectivamente. Por último, una clase 100% abstracta o que tiene todos sus métodos abstractos se denomina una interfaz. 100 Polimorfismo Suena complicado pero en realidad es algo muy simple. El polimorfismo viene del griego 'muchas formas' y con el siguiente ejemplo entenderemos a qué se refiere. Pensemos que ya hemos terminado todas las subclases de IPod para todos los modelos. Imaginemos que en alguna parte del código hemos permitido que los IPOD se dañen, como puede ocurrir con un IPOD real. Podemos entonces crear una clase independiente a todos ellos que es la encargada de reparar los IPOD llamada Reparar. Con lo que sabemos hasta ahora podemos permitir que el constructor de esta clase reciba un objeto a reparar, por ejemplo podríamos permitir que esta clase reparara objetos de la clase Nano de la siguiente forma: class Reparar { public Reparar(Nano nano) { // Código para reparar el objeto Nano que se pasa a este constructor } } Recordemos que para recibir parámetros en un método o constructor, escribimos dentro del paréntesis el tipo seguido del nombre que queramos asignarle. En el código anterior estamos creando un constructor para la clase Reparar en el que recibimos un objeto de tipo Nano y que hemos llamado nano para usar este nombre dentro del bloque para hacer referencia al objeto pasado, pero bien pudimos poner cualquier nombre. El problema con el código es que sólo está recibiendo los IPod Nano, las otras subclases de IPod no podrían entrar en Reparar. Es por esto que aparece el polimorfismo, que es la habilidad que nos da la programación orientada a objetos para pasar todas las subclases de IPod usando precisamente la clase IPod como tipo de parámetro dentro del paréntesis del constructor. Entonces si queremos recibir todas las subclases podemos proceder así: 101 class Reparar { public Reparar(IPod ipod) { // Código para reparar todo objeto IPod o sus subclases } } Recordemos que no podemos crear objetos directamente de la clase IPod porque dijimos que debería ser una clase abstracta. Si no la marcáramos abstracta y pudiéramos crear objetos de tipo IPod, también podríamos pasarlos a la clase Reparar. Lo que hace a este código polimórfico es que especifica una clase que tiene subclases, por lo tanto puede entrar tanto la superclase IPod como las subclases Nano, Touch, etc. Sin la programación orientada a objetos y el polimorfismo, tendríamos que crear diferentes códigos para poder reparar cada uno de los modelos de IPod. Lo bueno es que así creemos muchas subclases en el futuro, todas pueden entrar en la clase Reparar. Gracias al polimorfismo también podemos crear variables de referencia de la siguiente forma: IPod nano = new Nano(); En este caso estamos especificando el tipo IPod pero en realidad estamos creando una subclase Nano. Gracias a este principio podemos entonces crear un arreglo de muchos IPod de la siguiente forma: public class Main { public static void main(String[] args) { Nano nano = new Nano(); Touch touch = new Touch(); IPod[ ] ipods = {nano, touch}; System.out.println(ipods[1]); } 102 } abstract class IPod { // código para IPod } class Nano extends IPod { // código para Nano } class Touch extends IPod { // código para Touch } Observa que en el código anterior estamos creando un arreglo de tipo IPod pero en su contenido estamos metiendo subclases del mismo. Esto es polimorfismo. Si por ejemplo sabemos que todos las subclases de IPod tienen o heredan un método llamado play(), podemos usar un ciclo de arreglos como for para el arreglo anterior y así podemos hacer play() en todos ellos al tiempo: for(IPod ipod : ipods) { ipod.play(); } Cada vez que creamos un objeto, éste automáticamente tiene una superclase llamada Object que ya está creada por Java. En nuestro ejemplo, IPod es una subclase de Object así no lo hayamos especificado. Todo objeto en Java tiene a Object como su superclase. Object tiene sus propios métodos, por ejemplo .getClass() es uno de sus métodos y como nuestros objetos son subclases de éste, entonces podemos llamar este método como si nosotros mismos lo hubiéramos declarado: System.out.println(nano.getClass()); 103 Suponiendo que nano es una variable de referencia a Nano, podemos poner el código anterior en main() o en donde hayamos creado el objeto nano y obtendremos de qué clase es dicho objeto en la ventana de salida. Podemos usar este método por simple herencia. Object nos sirve para hacer declaraciones polimórficas como las que hemos visto antes, si queremos que algo sea lo suficientemente genérico como para que quepan muchos tipos de objetos que no están relacionados: Object[ ] arreglo = {nano, touch, carro, helado, cuaderno}; En el código anterior estamos suponiendo que cada elemento dentro del arreglo es una variable de referencia a un objeto creado por nosotros. Por ejemplo supongamos que tenemos una clase llamada Carro que nos permite crear carros y la hemos puesto en una variable de referencia llamada carro. Gracias al polimorfismo y gracias a que todos los objetos en Java son subclases de Object, podemos hacer este tipo de arreglos lo suficientemente genéricos para que quepan objetos que no se relacionan entre sí. Debemos ser cuidadosos cuando tenemos la siguiente situación. Imaginemos que creamos un método el cual recibe un objeto del tipo IPod y luego lo devuelve del tipo IPod. Por polimorfismo podemos pasar un Nano, pero como el método devuelve el tipo IPod, vamos a recibir un objeto Nano envuelto en un IPod. Esto quiere decir que ya no podremos tratarlo como un Nano, si por ejemplo tratamos de llamar métodos propios y únicos de Nano no vamos a poder realizarlos porque el programa cree que es un IPod y no un Nano. En este caso debemos hacer un cast. Recordemos que hacer casting significa cambiar el tipo de una variable o un valor usando dentro del paréntesis el tipo deseado y anteponiéndolo al valor que deseamos convertir, si estamos seguros que el objeto devuelto es un Nano: Nano nano = (Nano) funcionQueDevuelveObjetoIPod(referenciaNano); 104 Clases externas Para poder usar clases externas debemos especificar paquetes. Un paquete no es más que una carpeta que contiene nuestros archivos, pero además esta estructura de carpetas debe declararse dentro del código. En resumen necesitamos crear paquetes que se llamen igual que las carpetas. Cuando creamos una nueva aplicación de Java en NetBeans, podemos escoger el paquete o carpeta en la que vamos a meter nuestro archivo principal donde dice Create Main Class, podemos escribir aquí el paquete en minúsculas, seguido de un punto y el nombre que le queremos poner a la clase que contiene main(). Por ejemplo podemos crear una aplicación llamada Paquetes, y en Create Main Class podemos poner base.Main que significa que nuestra carpeta que va a contener la clase principal se va a llamar base y dentro vamos a tener un archivo llamado Main que va a contener main(): Con esto tenemos nuestro primer paquete declarado dentro del archivo Main. Podemos ver que NetBeans ha creado la estructura básica de este archivo y además agregó al comienzo la siguiente línea de código: 105 package base; Esta línea de código es la forma en que declaramos un paquete en un archivo de Java. En este caso estamos diciendo que el paquete se llama base, esto significa que el archivo se encuentra dentro de una carpeta llamada base. Ya NetBeans se encargó de nombrar y crear correctamente la carpeta. Como ya lo he dicho, el nombre de la carpeta y el nombre del paquete deben coincidir. Hasta ahora sólo tenemos un archivo así que no hemos ganado nada con lo que acabamos de hacer, pero ahora que vamos a empezar a crear más archivos veremos las ventajas ya que esto nos permitirá comunicar clases externas. Primero creemos otro archivo dentro del mismo paquete para crear allí otra clase que vamos a usar dentro de Main. Para esto simplemente vamos a File > New File y allí escogemos Java Class. En el nombre escribimos InBase que va a ser el nombre de la clase y en Package dejamos base. Con esto veremos que NetBeans crea por nosotros un archivo llamado InBase.java dentro de la misma carpeta base. El programa también nombra por nosotros el paquete correcto dentro del código. Si ponemos el siguiente código dentro de Main estaremos usando la clase externa: package base; public class Main { public static void main(String[] args) { InBase in = new InBase(); System.out.println(in.getClass()); } } Aquí estamos creando un objeto de la clase externa InBase que debe ser public para poder usarla. Podríamos no haber usado los paquetes e igual este código funcionaría ya que ambos archivos están en la misma carpeta. 106 Cuando tenemos carpetas diferentes es cuando empiezan a hacerse útiles los paquetes. Para crear una nueva carpeta o un nuevo paquete como se llaman en Java, vamos a File > New File y seleccionamos Java Package. Automáticamente se nos llena el nombre empezando en base seguido de un punto, lo que queremos cambiar es el nombre que va justo después del punto, vamos a nombrar este paquete ext así que después del punto escribimos ext. En este caso se nos crea una carpeta llamada ext. En la ventana Projects podemos ver que se nos ha creado una nueva carpeta llamada base.ext, en realidad la carpeta si la buscamos en nuestro computador solo se llama ext, pero NetBeans la nombra así para referenciarnos el paquete. Todo punto en Java significa que lo que le sigue al punto está contenido en lo que está justo antes. Es exactamente como ocurre cuando llamamos un método desde una variable de referencia que usamos un punto. Si hacemos clic derecho sobre base.ext en la ventana Projects podremos crear un nuevo archivo dentro de este paquete si hacemos clic en New > Java Class. Como nombre de clase ponemos OutBase y nos aseguramos que el nombre del paquete sea base.ext. Inmediatamente se nos crea un archivo llamado OutBase.java dentro de la carpeta ext. Observa que el paquete se ha declarado completo de la siguiente forma dentro de nuestro nuevo archivo: 107 package base.ext; Cuando estemos declarando paquetes de varias carpetas usamos la sintaxis de punto para poder referenciarlas completas tal y como lo hizo NetBeans por nosotros. Ahora podemos ir a Main y podemos tratar de hacer un nuevo objeto de esta clase de la siguiente forma: OutBase out = new OutBase(); System.out.println(out.getClass()); Si tratamos de compilar obtendremos errores. Es entonces el momento de usar los paquetes. Una vez creado un paquete como lo hemos hecho debemos importarlo cuando los archivos o clases a relacionar no se encuentren en la misma carpeta. Lo que debemos hacer en este caso es importar el paquete y la clase que vamos a usar justo antes de la declaración de la clase. Así debe verse nuestro archivo llamado Main con la declaración de importación: package base; import base.ext.OutBase; public class Main { public static void main(String[] args) { InBase in = new InBase(); System.out.println(in.getClass()); OutBase out = new OutBase(); System.out.println(out.getClass()); } } Observa que lo nuevo es la segunda línea. Para importar una clase externa simplemente escribimos import, dejamos un espacio y escribimos el paquete que en este caso es base.ext y luego usando sintaxis de punto agregamos el nombre 108 de la clase que queremos importar y que en este caso es OutBase. Si tuviéramos muchas clases dentro del paquete base.ext, podríamos importarlas todas usando la siguiente línea: import base.ext.*; El símbolo * que antes hemos usado para multiplicar, lo usamos ahora como comodín para referirnos a todo el contenido del paquete. Como una alternativa, pudimos no importar la clase sino nombrarla con todo y su paquete al momento de usarla. Mira como se hace esto, igualmente usando sintaxis de punto: base.ext.OutBase out = new base.ext.OutBase(); Debemos usar la estructura de paquetes para ordenar el código. En muchas aplicaciones grandes podemos tener cientos de clases y es mejor ponerlas en paquetes significativos, por ejemplo podemos crear un paquete llamado en mi caso juanlopera en el que voy a poner todas mis clases que después podré reusar en otros proyectos. Dentro puedo poner una carpeta llamada audio y otra llamada midi y cada una va a contener mis clases para poder crear objetos para dichos temas. Esto va a ayudar mucho cuando esté trabajando en otras aplicaciones, además usar un nombre como juanlopera va a asegurar que mis archivos estén en un paquete único lo que no va a permitir la colisión de nombres si otro programador usó nombres iguales a los míos en sus clases. Java tiene sus propias librerías donde hay miles de clases. De hecho la versión de Java que estoy usando para este proyecto de grado, que es Java 1.6, tiene 3793 clases. De éstas ya hemos usado varias sin saberlo, por ejemplo String, Math y System son todas clases de Java, pero todavía nos quedan miles por descubrir. Obviamente no es necesario saberlas todas, hay muchas muy específicas para temas que no nos interesan y tal vez nunca las usemos. Con esto quiero demostrar el poder de Java. Si bien hasta ahora hemos visto un lenguaje muy 109 poderoso, nos falta muchísimo por descubrir y este trabajo no pretende ni puede cubrirlo todo. Si bien nos falta mucho por descubrir, hemos cubierto muchas de las bases del lenguaje y de aquí en adelante nos queda por cubrir partes muy importantes de la librería de Java. Por ejemplo para poder crear interfaces gráficas para no tener que usar más la ventana de salida, para eso usaremos una parte de la biblioteca llamada swing que veremos más adelante. Las clases que hemos usado de la librería de Java no necesitaban ser importadas porque todas viven en un paquete llamado java.lang que está importado por defecto en todas nuestras aplicaciones. Sin embargo esto sólo ocurre porque allí viven las clases más comunes de Java. Por ejemplo la parte gráfica antes mencionada vive en un paquete llamado javax.swing que es necesario importar para trabajar con sus clases. Más adelante cuando aprendamos a crear interfaces gráficas para los usuarios, veremos que vamos a usar mucho este paquete y para importarlo procedemos así: import javax.swing.*; En este caso estamos importando el paquete javax.swing y con nuestro comodín estamos importando todas las clases que se encuentran allí. Si más adelante aprendes por tu cuenta que hay una clase que te va a ser muy útil y que no enseño aquí, puedes ir por tu cuenta y ver en qué paquete se encuentra. Si dicha clase está dentro de java.lang ya sabes que no tienes que importar nada. Si por el contrario te enteras que no hace parte de ese paquete, simplemente tienes que importar el paquete, luego escribes un punto y la clase que quieres traer. Recuerda que con el comodín puedes traer todas las clases de un paquete. Cuando lleguemos al punto álgido de este proyecto de grado que es el manejo del audio, veremos que la librería encargada del audio en Java debe ser importada y se divide en dos paquetes, una para manejo de MIDI y otra para el manejo del audio en sí. Los dos paquetes son javax.sound.sampled para el manejo de audio y javax.sound.midi para el manejo de MIDI. 110 Excepciones Cuando trabajamos con audio o MIDI, encontramos muchos comportamientos inseguros. Por ejemplo, cuando queremos empezar a programar aplicaciones MIDI, debemos pedir al programa que busque los sintetizadores disponibles, pero podríamos encontrarnos con situaciones en las que no haya ningún sintetizador disponible para que la aplicación funcione. En este caso y muchos otros, Java ha determinado que hay ciertos comportamientos que son riesgosos, otro ejemplo es tratar de abrir un archivo que no existe, y han creado las excepciones. Las excepciones no son más que formas de resolver las situaciones inseguras. Para lograr esto, los métodos pueden generar excepciones. Nosotros mismos podemos crear métodos que arrojen excepciones, pero también entre las miles de clases que Java ya ha creado, muchos de sus métodos que se consideran inseguros, arrojan excepciones cuando algo sale mal. De forma muy general, debemos usar la siguiente estructura para manejar un método que arroje excepciones: try { // aquí va el método inseguro que arroja excepción. } catch (Exception ex) { // aquí escribimos el código por si algo sale mal. } De forma simple, dentro del bloque try escribimos el método que arroja la excepción, esto decir que allí escribimos el comportamiento que no es 100% fiable. Luego de ese bloque escribimos catch seguido de unos paréntesis que contienen un objeto del tipo Exception y que hemos llamado ex, este objeto es la superclase de toda excepción. Esto quiere decir que las excepciones son objetos, si bien cada método puede generar objetos diferentes, todos ellos son subclases de Exception. En este ejemplo simple pusimos la superclase para ser lo más 111 polimórficos posibles. Cuando escribimos dentro de un mismo bloque try varios métodos que se consideran inseguros, pero que arrojan diferentes objetos, podemos escribir en este paréntesis la superclase para poderlos recibir todos. Para entender las excepciones usemos primero un ejemplo real de MIDI en Java, adelantándonos un poco a códigos que veremos a fondo más adelante. Cuando vamos a crear un secuenciador usamos objetos, Java los ha nombrado Sequencer. Este es el tipo que debemos usar en la variable de referencia al objeto. Recordemos que para crear una variable de referencia a un objeto simplemente escribimos el tipo, seguido del nombre que queramos y lo igualamos a una nueva instancia del objeto. Sin embargo para obtener un Sequencer debemos proceder un poco diferente, en el capítulo de MIDI entenderemos a fondo que ocurre en el siguiente código, por ahora hay que hacer acto de fe y saber que para tener un nuevo secuenciador usaremos el siguiente código: Sequencer secuenciador = MidiSystem.getSequencer(); La línea anterior puede generar una excepción llamada MidiUnavailableException. En realidad es el método getSequencer() el que arroja este objeto. Como ya se mencionó, esta excepción es una subclase de Exception. Como el código anterior arroja una excepción debemos tratarlo de la siguiente forma: try { Sequencer secuenciador = MidiSystem.getSequencer(); } catch (MidiUnavailableException ex) { System.out.println(ex); } Como puedes ver, simplemente pusimos el código inseguro dentro del bloque try y luego en el paréntesis de catch pusimos el objeto que arroja el método inseguro y lo nombramos ex para luego imprimirlo en la ventana de salida. Dentro del bloque 112 catch estamos haciendo algo muy simple, en una aplicación real debemos manejar la inseguridad de forma robusta, por ejemplo indicándole al usuario si hizo algo mal o una verdadera solución al problema. Si tratas de escribir el código anterior, obtendrás un error a la hora de compilar porque no hemos importado los paquetes necesarios para trabajar con MIDI. Como recordarás sobre el capítulo de clases externas, hay ciertos paquetes que debemos importar para poder usar ciertas clases y métodos de Java. Para trabajar con MIDI debemos importar sus métodos y clases de la siguiente forma: import javax.sound.midi.*; Este código debemos escribirlo al comienzo del archivo antes de las clases. Con este código si podemos escribir el try y catch antes vistos y vamos a poder compilarlo si lo escribimos correctamente dentro de main(). Al compilar ejecutar el archivo nada debe pasar si se creó un Sequencer correctamente y si no obtendremos el error en la ventana de salida. Si lo deseamos, nosotros mismo podemos escribir métodos que arrojen excepciones. El siguiente código es un ejemplo fuera de contexto y declara una excepción para el método riesgoso(): public void riesgoso() throws MiExcepcion { if(algoSalioMal) { throw new MiExcepcion; } } En este caso imaginemos que tenemos una variable boolean llamada algoSalioMal que se convirtió en true, el método botará una excepción del tipo MiExcepcion que es una clase que debemos crear como ya veremos más adelante. Lo importante aquí es que aprendamos que cuando queremos que un 113 método arroje una excepción simplemente escribimos después de sus paréntesis la palabra throws seguida de la clase que contiene la excepción que en este caso es MiExcepcion. Dentro de este método en algún punto que consideremos que salió algo mal, debemos arrojar la excepción usando las palabras throw new seguidas del tipo de clase que contiene la excepción. Para usar el método anterior debemos proceder de la siguiente forma, analiza el siguiente código que si compila: public class Main { public static void main(String[] args) { Main main = new Main(); try { main.riesgoso(); System.out.println("Todo salió bien"); }catch(MiExcepcion ex) { System.out.println(ex); } } public void riesgoso() throws MiExcepcion { boolean algoSalioMal = true; if(algoSalioMal) { throw new MiExcepcion(); } } } class MiExcepcion extends Exception{ public MiExcepcion() { super("Algo salió mal"); } } 114 Aquí tenemos dos clases: Main y MiExcepcion. La clase Main contiene main() y el método que hemos llamado riesgoso() que es inseguro y por lo tanto arroja una excepción que hemos creado nosotros mismos y es la clase MiExcepcion que como puedes ver debe extender Exception. Esta clase simplemente llama desde su constructor, al constructor de su superclase Exception usando la palabra clave super() que es capaz de recibir un mensaje que sale en pantalla. Trata de compilar este código y verás el error en la ventana de salida. Si cambias la variable algoSalioMal a false, verás un mensaje que dice "Todo salió bien". Analiza este código ya que contiene muchos de los temas que hemos visto hasta ahora. Podemos concluir que los métodos que Java considere que pueden llegar a presentar errores, arrojan excepciones. Estas excepciones son subclases de Exception lo que nos permite hacer catch polimórficos. Es por esto que cuando nosotros mismo estamos creando excepciones, debemos extender la clase Exception. Si un método arroja una excepción debemos usar la palabra throws después del paréntesis de parámetros, seguida de la clase que extiende Exception. En algún punto de ese método debemos arrojar el error escribiendo throws new seguida de la clase que contiene la clase correcta. Por lo general usaremos muchos métodos en audio que arrojen excepciones, es por eso que lo más importante de este capítulo es que aprendamos que cuando un método tiene esta habilidad, debemos manejarlo usando bloques try y catch. Cuando un método arroja una excepción es obligatorio meterlo en un try. En el bloque catch podríamos no hacer nada y el código compilará, pero la mejor práctica en aplicaciones reales es solucionar el posible problema o al menos informar al usuario. 115 Multihilos Hoy día los computadores modernos tienen varios procesadores para poder realizar varias tareas a la vez y trabajar más rápido. Como ya dije antes, el código en Java se ejecuta de arriba hacia abajo y de izquierda a derecha. Sin embargo hay muchas ocasiones en las que queremos ejecutar porciones de código simultáneamente. Aunque esto es posible en Java, no funciona como en los procesadores en un computador que es un escenario en el que de verdad se usan dos o más procesadores para realizar tareas distintas. En el caso de Java es simplemente una simulación, usando un solo procesador los multihilos nos permitirán tratar de recrear un escenario en el que dos o más códigos estén siendo ejecutados al tiempo pero no olvidemos que es sólo una simulación que funciona bastante bien. En el caso del audio, un buen ejemplo de la utilidad de la tecnología multihilos es cuando estamos capturando el sonido del micrófono. Cuando veamos este código aprenderemos que se hace mediante un ciclo que dura mientras queramos mantenernos capturando la señal. Sin embargo en la mayoría de aplicaciones reales queremos poder mantenernos en ese ciclo de captura del micrófono y además permitirle al usuario realizar otros trabajos en la aplicación. La solución a este problema es usar multihilos. De por si las aplicaciones como las hemos creado hasta ahora ya usan un hilo. Para crear un segundo hilo simplemente creamos el código que queramos que corra como tarea alterna en una clase, para este ejemplo la vamos a llamar SegundoHilo pero puedes ponerle el nombre que quieras. Esta clase debe implementar Runnable que es una interfaz creada por Java que no debemos importar porque existe dentro de java.lang. Esta interfaz tiene un único método llamado run(). Recordemos que toda interfaz que implementemos debemos sobrescribir sus métodos, así que nuestra clase SegundoHilo que implementa Runnable debe sobrescribir run() que es el método que se ejecuta 116 automáticamente cuando creamos este nuevo hilo. El siguiente es el código necesario para empezar el nuevo hilo SegundoHilo. public class Main { public static void main(String[] args) { Runnable independiente = new SegundoHilo(); Thread miHilo2 = new Thread(independiente); miHilo2.start(); System.out.println("Hola desde hilo principal."); } } class SegundoHilo implements Runnable { public void run() { System.out.println("Hola desde segundo hilo."); } } Crear una aplicación multihilos es muy fácil. Creamos una clase aparte que carga el código que se ejecuta como hilo independiente. En este caso la hemos nombrado SegundoHilo y como vemos debe implementar la interfaz Runnable. Esta clase puede tener todos los métodos que quieras pero debe tener uno llamado run() que se ejecutará automáticamente cuando corre el hilo. Para empezar el segundo hilo creamos una variable de tipo Runnable, también puede ser el nombre que le hayamos puesto a la clase pero por polimorfismo estamos usando Runnable. esta variable es igual a una nueva instancia de nuestra clase que va a ejecutarse, la hemos llamado independiente. Luego creamos una variable de tipo Thread que es la encargada de los hilos, la hemos llamado miHilo2. El constructor de esta clase recibe una instancia de tipo Runnable así que le pasamos nuestra primera variable y ya con esto podemos empezar el hilo usando la referencia a Thread que es miHilo2.start(). si lo queremos podemos 117 crear varios hilos a la vez, obviamente si son muchos y dependiendo del sistema, la aplicación va a tender a hacerse lenta. Como dije antes, estos multihilos son simulaciones, esto quiere decir que en verdad no están ocurriendo al tiempo sino que están ocurriendo por partes. Por ejemplo primero ocurre una porción de un hilo, después otra del otro hilo, luego vuelve al primero y así sucesivamente. Lo que pasa es que ocurre tan rápido que pareciera que ocurren al tiempo. Nosotros no tenemos control para saber cuál de dos o más hilos va a terminar primero, esto depende de muchos factores como el sistema operativo, la máquina virtual Java y otros procesos, pueden acabar unos hilos después o antes y si volvemos a correr la aplicación terminan diferente. Lo único que podemos hacer es parar por cierto tiempo un hilo que ya se esté ejecutando. Para esto escribimos el siguiente código dentro de la clase del hilo que queramos parar por un tiempo: Thread.sleep(2000), el valor dentro del paréntesis es el tiempo en milisegundos que queremos mantenerlo dormido. El tema multihilos es muy extenso y aquí apenas hago introducción a éste ya que lo necesitaremos en nuestras aplicaciones de audio. Sin embargo con estas bases podemos crear nuestras primeras aplicaciones. La gran mayoría de aplicaciones creadas en Java usan programación multihilos pero debemos saberla usar. el comportamiento de estos multihilos cambia de máquina a máquina así que todas tus aplicaciones deberían ser probadas en la mayor cantidad de computadores posibles. Si bien Java es suficientemente portable para escribir una vez el código y poder correr las aplicaciones casi en cualquier parte, esto no quiere decir que debemos ser descuidados como programadores, todas nuestras aplicaciones deben ser probadas siempre en la mayor cantidad de ambientes, en computadores lentos y rápidos, e incluso es buena idea abrir muchos programas en nuestro computador y luego abrir la aplicación que hayamos creado para ver cómo se comporta, probando sus límites. 118 Estáticos Si bien la idea de las clases es poder crear objetos, muchas veces queremos contener código dentro de una clase por organización pero queremos usar sus métodos de forma más fácil y rápida que creando un objeto. Pensemos por ejemplo en la clase de Java Math. Si queremos usar random() que es uno de sus métodos, no tenemos que hacer un objeto de Math para poder usarlo. Recordemos que para crear un número aleatorio entre 0 y casi 1 simplemente escribimos el siguiente código donde lo necesitemos: Math.random();. Nunca tuvimos que escribir una variable de referencia a Math. Existen muchas clases como Math de las cuales no queremos hacer objetos, más bien son clases para usar sus métodos como utilidades rápidas. A veces no es toda la clase sino son métodos específicos e incluso variables dentro de objetos que queremos tratar de una forma especial. Para esto usamos la palabra clave static que no es más que un modificador que nos permite ser un poco más flexibles con los objetos. La palabra static la podemos usar en métodos o en variables. Todos los métodos en Math están declarados como static y esto es lo que nos permite acceder a ellos a través del nombre de su clase sin necesidad de hacer un objeto. En el mundo del audio es una buena idea crear una clase que nos permita tener utilidades rápidas listas para usar. Probemos el siguiente código: public class Estaticos { public static void main(String[] args) { AudioRapido.formatoCD(); } } class AudioRapido { private AudioRapido() { 119 } public static void formatoCD() { System.out.println("Sample rate: 44100KHz"); System.out.println("Bit depth: 16bits"); } } Como todavía no sabemos nada de audio, no puedo adelantarme tanto y crear un método estático que realmente nos sea útil, pero este ejemplo simple nos permite entender para qué sirven los métodos estáticos. En el ejemplo anterior estamos usando el método formatoCD() desde main() sin necesidad de crear una variable de referencia al objeto. Esto lo podemos hacer gracias a la palabra static. Observa también que he creado un constructor privado para la clase AudioRapido que por dentro está vacío, con esto lo que pretendo es que no se puedan hacer objetos de esta clase ya que no está pensada como una clase para objetos sino como un simple contenedor para varios métodos útiles, a los cuales queremos acceder rápido. La clase Math de Java está declarada de la misma forma que hemos creado nuestra clase AudioRapido. Aunque uno, varios o todos los métodos de una clase pueden ser estáticos, debemos ser cuidadosos al escribirlos. Por ejemplo un método estático no puede verse afectado por variables de instancia. Las variables de instancia son las variables que comparten todos los métodos dentro de una clase, éstas son las variables declaradas dentro de una clase pero fuera de los métodos: class Clase{ int entero; // variable de instancia. Está dentro de la clase fuera de un método. } Estos métodos estáticos deben tener y usar sólo sus propias variables, por ejemplo la variable mostrada en el ejemplo anterior no puede ser usada por un 120 método estático ya que está declarada fuera de los métodos. Las variables creadas dentro de un método son llamadas variables locales porque sólo existen allí, desde fuera nadie las puede ver ni usar. Entonces un método estático debe usar solo variables locales aunque si puede recibir parámetros y devolver valores como el resto de métodos para poderse comunicar con el resto del código. Básicamente los métodos estáticos nos alejan de la programación orientada a objetos, esto es bueno solo cuando necesitamos métodos que funcionen aparte de los objetos pero perfectamente podemos tener una clase que nos permita crear y objetos y dentro podemos tener uno o varios métodos estáticos. Éstos se usarían para hacer algo general y no relacionado a una instancia específica. Las variables también pueden ser estáticas. Son muy útiles para que todos los objetos de una clase compartan un mismo valor para una variable. Por ejemplo imaginemos para nuestra clase Nano, que es subclase de IPod, si quisiéramos saber cuántos IPODs se han creado. Para saber cuántas variables de referencia al objeto Nano se han creado podemos declarar la siguiente variable de instancia: class Nano extends IPod{ private static int cantidadNanos = 0; public Nano () { cantidadNanos ++; } } En este caso declaramos la variable de instancia cantidadNanos privada para que no pueda ser modificada fuera del código. Cada vez que se crea una nueva instancia de Nano, el constructor aumenta la variable estática que es compartida por todos los objetos de tipo Nano. Si creamos un método que nos permita obtener la variable estática, por ejemplo se puede llamar getCantidad(), podríamos saber la cantidad sin importar desde cuál objeto Nano la llamemos ya que cantidadNanos es igual para todos los objetos, todos la comparten por ser static. 121 ¿Qué es un API? API significa Application Programming Interface. De forma muy simple un API no es más que un código que está escrito para satisfacer las necesidades de un tema específico que normalmente es ampliamente usado y aunque el código interno puede ser complejo, al estar en un API se vuelve más fácil de manejar y por ser una interfaz debemos aprender a usar. Por ejemplo todas las clases que se encargan de manejar el audio en Java se dice que están en el API del sonido de Java. Otro ejemplo, cuando una página de internet se vuelve muy famosa como Facebook o Twitter, muchas otras páginas y programas terceros quieren poder usar sus aplicaciones desde sus propias páginas. Para lograr esto, los programadores de Facebook y Twitter crean sus propios APIs que no son más que códigos escritos en ciertos lenguajes de programación que nos permiten a nosotros acceder a sus funciones desde nuestros códigos sin vulnerar la seguridad de ellos ni la nuestra. En Java un API son los medios que nos da este lenguaje para desarrollar aplicaciones. Existen APIs para audio, otras para interfaces gráficas, otras para manejo de 3 dimensiones y también existen cientos de APIs creadas por terceros que podemos descargar desde Internet. Por ejemplo existe un API para reconocimiento de voz que se puede comprar o descargar que se encarga de que nosotros podamos poner en nuestras aplicaciones reconocimiento de voz sin necesidad de saber sobre la matemática envuelta en este proceso. Aunque nosotros mismos podríamos crear y escribir los algoritmos para reconocimiento de voz, es mucho más fácil y rápido buscar el API de reconocimiento de voz y aprender a usarlo en nuestro código. Nosotros mismos podemos crear un API de audio, éste no sería más que una serie de clases pensadas y organizadas de forma lógica para usarse en conjunto y que permitirían trabajar con el audio y agregar funciones al audio a nuestro gusto. Este API no es sólo para terceros, nosotros mismos podríamos usarlo para no 122 reescribir código innecesariamente. Lo que haríamos sería crear un paquete en el que escribiríamos todo nuestro código. Como vimos en el capítulo de clases externas, podríamos crear un paquete llamado juanlopera.audio en el que tuviéramos todas las clases de nuestro API de audio. Existen libros dedicados a la forma en que deben pensarse y organizarse los códigos para un API. Aunque este tema es muy amplio y complejo como casi todos los que involucran el mundo de la programación, es importante que sepamos que el verdadero poder de los lenguajes se encuentra en el correcto uso de APIs bien escritos. Un buen punto de partida son los APIs que trae Java. De hecho este proyecto de grado está enfocado para que al final se pueda entender de forma general el API de sonido en Java que envuelve tanto audio como MIDI. Muy pocas veces es buena idea reinventar la rueda, así que lo mejor es que empecemos a aprender a usar los APIs de Java que nos van a permitir crear aplicaciones robustas. No los vamos a poder aprender todos pero si podemos aprender algunos necesarios. Además del API de audio en Java también veremos el API para poder crear un GUI, que son las interfaces gráficas para que podamos mostrarle al usuario imágenes, botones, animaciones y demás. Como conclusión es importante que entiendas que en programación, no solo en Java, existen los APIs que son muy útiles para usar de forma más fácil y correcta un tema específico. Tú mismo puedes crear APIs, pero es más probable que los uses. Aparte de los que enseño aquí, vas a encontrar muchos otros que son muy útiles y que al aprender van a mejorar tus aplicaciones. Algunos de ellos tendrás que aprender a instalarlos para poder usarlos y otros ya vienen incluidos con Java. Como los API se encuentran en paquetes, es probable que de aquí en adelante la mayoría de ejemplos o aplicaciones que creemos necesiten importar el respectivo paquete para poder usarlo. Este proyecto de grado no usa APIs externos, solamente los que vienen con Java SE. 123 Como Java tiene tantas clases diferentes, no necesitamos aprenderlas todas pero si necesitamos aprender a entender la documentación que nos brinda Java para poder usar su API. Es importante que veamos una rápida mirada a cómo buscar información. En la siguiente dirección podemos ver la documentación del API de Java 6SE: http://download.oracle.com/javase/6/docs/api/ allí podemos ver que la página está ordenada en tres recuadros: En la ventana 1 aparecen los paquetes en los que organiza Java todas sus clases. Si usamos clases de un paquete debemos importarlo al comienzo de nuestro archivo. El único paquete que no es necesario importar es java.lang. Si hacemos clic en éste veremos que en la ventana 2 aparecerán las clases contenidas dentro de este paquete. Si observamos la lista de clases encontraremos System, String, Throwable, Math y Object que son las clases que hemos usado en este proyecto de grado, como todas ellas viven dentro de java.lang no hemos necesitado importar el paquete. Si hacemos clic en la clase String veremos que la ventana 3 se actualiza. En la ventana tres es donde veremos todo el contenido especificado de una clase. Al comienzo encontramos la explicación de para qué sirve la clase String o la 124 clase seleccionada. Más abajo encontramos tanto el resumen de todos los constructores de la clase como todos los métodos. Por ejemplo, si buscamos el método equals() que usamos en el capítulo de sentencias de prueba, veremos lo siguiente: La casilla izquierda especifica el tipo de retorno que nos devuelve el método. En este caso dice que cuando usamos el método equals() recibimos de vuelta un valor de tipo booleano que podemos capturar en una variable o simplemente poner en una sentencia de prueba. En la casilla derecha vemos en azul la palabra equals seguida de un paréntesis que contiene el tipo de objeto que se puede pasar dentro del paréntesis, en este caso podemos pasar cualquier objeto ya que el tipo es Object que recordemos que es la superclase de cualquier otro objeto, así que por polimorfismo podemos pasar lo que queramos. En la documentación de Java los parámetros que reciben los métodos los encontramos especificados de esta forma (Object anObject) Primero dicen el tipo de objeto que debe pasarse que en este caso es Object, luego escriben un nombre significativo cualquiera, en este caso lo han llamado anObject, pero bien pudo ser cualquier nombre. Lo importante siempre es mirar la primera palabra que identifica el tipo de parámetro que recibe el método. Por último vemos una breve descripción de lo que hace el método. Si hacemos clic sobre éste, veremos una explicación más detallada. Mirando los métodos de String puedes ver que hay uno llamado endsWith() que sirve para saber si un texto termina en lo que sea que estemos probando. Este 125 método devuelve un booleano así que podemos usarlo en una sentencia de prueba. Suena útil y para usarlo podemos proceder de la siguiente forma: String texto = "cantar"; if(texto.endsWith("ar")) { System.out.println("Es muy probable que la palabra sea un verbo"); } Así podemos seguir buscando a través del API y encontrarnos con otros métodos más útiles todavía. Es muy útil e importante que trates de estar yendo a la documentación y aprender un poco cada día más sobre Java. Normalmente usar cualquiera de los buscadores más famosos como Google es suficiente para ir directamente al tema del API que estamos buscando. Por ejemplo podemos buscar 'java string' y entre los primeros resultados buscamos el que tenga una dirección que empiece por download.oracle.com allí estaremos directamente en la documentación del API de java para String. Esto es muy útil porque a partir de este punto puedes ir y aprender sobre los diferentes métodos que nos ofrece no sólo la clase String sino muchas otras de las clases. Además ya sabes que cuando necesites recordar o aprender sobre alguna clase o un paquete como por ejemplo el de audio, siempre puedes ir y buscar muy rápidamente en línea toda la documentación. Me ha pasado muchas veces que busco en línea sobre cómo resolver alguna situación de programación y veo que la solución es usar un método que no conozco, incluso clases que desconozco. En ese punto es buena idea referirse a la documentación de Java para ver qué podemos aprender sobre las posibilidades de esas clases y esos métodos, no es suficiente con quedarnos con los códigos que vemos por ahí ya que muchos pueden contener errores. 126 GUI Este capítulo no sólo es fácil sino que es muy agradable ya que aprenderemos a crear interfaces gráficas para que el usuario pueda ver e interactuar por medio de botones, texto, imágenes, etc. GUI significa Graphical User Interface que no es más que el nombre dado al conjunto de recursos visuales que nos permiten comunicarnos con un programa. Para este capítulo usaremos la librería Swing que es la encargada de crear interfaces gráficas de forma fácil. Antes de empezar, es muy importante saber que esta librería nos permite crear las ventanas, botones y campos, todos ellos denominados componentes, que son nativos del sistema. Esto quiere decir que cuando creamos un botón, la librería llama el aspecto visual del botón nativo del sistema. Si estás en Mac verás un botón de acuerdo con la versión del sistema operativo y si estás en PC verás otro aspecto distinto que también depende de la versión del sistema operativo, y así con todos los componentes. Como la librería swing se encuentra fuera de java.lang, debemos importarla de la siguiente forma: import javax.swing.*; Recordemos que esta sentencia debe ir antes de las clases en nuestro archivo y que el símbolo * significa importar todas las clases que se encuentren dentro de swing. Lo primero que debemos hacer después de importar nuestra librería es crear un contenedor denominado JFrame que es una ventana que va a incluir todos los componentes que vayamos a crear. Con el siguiente código creamos dicho contenedor: import javax.swing.*; public class Main { public static void main(String[] args) { JFrame ventana = new JFrame("Mi Primera Ventana"); ventana.setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE); 127 ventana.setSize(300, 300); ventana.setVisible(true); } } Si ejecutas este código verás una ventana en blanco que tiene las mismas características que una ventana abierta en tu sistema operativo, esto quiere decir con los tres botones para minimizar, agrandar y cerrar. Este es el aspecto que tiene en mi sistema operativo, Windows 7. Analizando el código vemos que en la primera línea de main() creamos una nueva referencia del objeto JFrame y a su constructor le pasamos el nombre que queremos que aparezca en la parte superior de la ventana, en este caso escribimos 'Mi Primera Ventana'. En las siguientes tres líneas usamos la variable de referencia al objeto para poder modificarlo. setDefaultCloseOperation(), En la al cual segunda le línea pasamos usamos la siguiente el método constante JFrame.EXIT_ON_CLOSE, este código es necesario para que cuando cerremos la ventana también se deje de ejecutar la aplicación, sin este código se cerraría la ventana con el botón pero la aplicación seguiría ejecutándose. En Java 128 reconocemos las constantes porque están escritas sólo en mayúsculas y sus palabras se separan con líneas de subrayado, tal como EXIT_ON_CLOSE. Una constante es una variable que nunca cambia su contenido, se crean igual que las variables y se marcan public static final. En la tercera línea usamos el método setSize() que recibe dos valores, primero el ancho en pixeles y después el alto en pixeles. Modifícalos a tu gusto para que entiendas cómo funcionan. En la última línea usamos el método setVisible() que nos permite volver visible o invisible la ventana recibiendo un booleano. Ahora empecemos a agregar componentes. Para crear un botón necesitamos agregar dos líneas a nuestro código anterior: import javax.swing.*; public class Main { public static void main(String[] args) { JFrame ventana = new JFrame("Mi Primera Ventana"); JButton boton = new JButton("¡Hazme clic!"); ventana.getContentPane().add(boton); ventana.setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE); ventana.setSize(300, 300); ventana.setVisible(true); } } Si compilamos y corremos este código, veremos que un botón se apodera de todo el contenido de la ventana JFrame. Para crear un botón se necesita una referencia al objeto JButton que tiene un constructor que recibe el texto que va sobre él mismo. Luego simplemente lo agregamos a la ventana JFrame usando los métodos getContentPane() y add() para poder visualizarlo. El método add() recibe la referencia al componente que queremos agregar. Observa que estamos usando 129 dos métodos seguidos en una misma línea usando sintaxis de punto, esto es totalmente válido. Como podemos ver, el botón se crea del ancho y alto máximos de la ventana que hemos creado, esto significa que nuestro botón tiene las dimensiones 300 pixeles de alto por 300 pixeles de ancho. Esto ocurre porque por defecto los componentes swing tienen su propia forma de ordenarse. La siguiente imagen muestra cómo se ve en mi computador el GUI anterior: Aquí podemos ver que toda la ventana es un botón al que podemos hacer clic. Cuando presionamos el mouse, vemos que el botón se oscurece un poco, este es el comportamiento típico de éstos. En el siguiente capítulo veremos cómo hacer para agregar eventos a los botones, esto quiere decir que ocurran acciones cuando hacemos clic sobre ellos. Casi nunca queremos crear un botón que nos ocupe toda la pantalla. Esto ocurre por defecto ya que swing tiene sus propias reglas para ordenar los diseños de los componentes. Si tratamos de agregar un segundo botón como hemos hecho hasta 130 ahora no podríamos. Para agregar más componentes debemos entender las cinco regiones que existen para poder ordenar nuestros elementos. Cuando creamos un JFrame, automáticamente tenemos 5 regiones a nuestra disposición: Por defecto, cada vez que agregamos un componente sin especificar la región en la que lo queremos, se agrega en CENTER. La forma correcta de agregar los componentes especificando la región es usando el constructor de add() que recibe tanto la región como la referencia al componente que va a agregar: ventana.getContentPane().add(BorderLayout.EAST, boton); En el código anterior estamos suponiendo que queremos agregar boton en la región EAST. Observemos que para especificar la región debemos escribir BorderLayout.EAST, pero BorderLayout es de por sí una clase que debemos importar para poder usarla: import java.awt.BorderLayout; Después de importarlo ya podemos usar BorderLayout.EAST. De esta forma sólo podemos agregar un componente por región ya que la ocupará toda. El siguiente código agrega un botón en cada una de las regiones: 131 import javax.swing.*; import java.awt.BorderLayout; public class Main { public static void main(String[] args) { JFrame ventana = new JFrame("Regiones"); JButton boton1 = new JButton("EAST"); ventana.getContentPane().add(BorderLayout.EAST, boton1); JButton boton2 = new JButton("CENTER"); ventana.getContentPane().add(BorderLayout.CENTER, boton2); JButton boton3 = new JButton("WEST"); ventana.getContentPane().add(BorderLayout.WEST, boton3); JButton boton4 = new JButton("NORTH"); ventana.getContentPane().add(BorderLayout.NORTH, boton4); JButton boton5 = new JButton("SOUTH"); ventana.getContentPane().add(BorderLayout.SOUTH, boton5); ventana.setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE); ventana.setSize(300, 300); ventana.setVisible(true); } } 132 Aquí vemos claramente la división de las 5 regiones. Como ya dije antes, en cada región sólo puede ponerse un componente, pero hay ciertos componentes que nos sirven para cargar varios componentes a la vez. Hasta ahora sólo hemos visto JFrame y JButton, pero en realidad hay muchos otros objetos dentro de swing que nos permiten crear cualquier interfaz que necesitemos. Si queremos poner varios botones dentro de una misma región podemos agregar un JPanel que no es más que un contenedor para otros componentes, allí podemos agregar varios botones. Para usarlo simplemente lo creamos, de la misma forma como procedemos con los botones, pero no necesitamos pasarle un argumento: JPanel contenedor = new JPanel(); ventana.getContentPane().add(BorderLayout.EAST, contenedor); Así estamos asignando un JPanel en la región EAST de nuestra aplicación. Lo único que tenemos que hacer es agregar los botones que queramos al JPanel: JButton boton1 = new JButton("Boton 1"); contenedor.add(boton1); En este código hemos agregado el botón ya no al JFrame sino al JPanel. Con el siguiente código estamos creando un botón para cada región, pero dos botones para la región EAST usando un JPanel: import javax.swing.*; import java.awt.BorderLayout; public class Main { public static void main(String[] args) { JFrame ventana = new JFrame("Regiones"); JPanel contenedor = new JPanel(); ventana.getContentPane().add(BorderLayout.EAST, contenedor); JButton boton1 = new JButton("Boton 1"); 133 contenedor.add(boton1); JButton boton2 = new JButton("Boton 2"); contenedor.add(boton2); JButton boton3 = new JButton("CENTER"); ventana.getContentPane().add(BorderLayout.CENTER, boton3); JButton boton4 = new JButton("WEST"); ventana.getContentPane().add(BorderLayout.WEST, boton4); JButton boton5 = new JButton("NORTH"); ventana.getContentPane().add(BorderLayout.NORTH, boton5); JButton boton6 = new JButton("SOUTH"); ventana.getContentPane().add(BorderLayout.SOUTH, boton6); ventana.setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE); ventana.setSize(400, 300); ventana.setVisible(true); } } El resultado es el siguiente: 134 En Java existen 3 grandes administradores de diseño: BorderLayout, FlowLayout y BoxLayout. Hasta ahora hemos usado BorderLayout que es el que nos permite ordenar en 5 regiones donde cada componente escoge su tamaño automáticamente. Dentro de la región EAST donde pusimos dos botones, allí también está ocurriendo otro administrador de diseño llamado FlowLayout que ordena de izquierda a derecha y de arriba hacia abajo cuando se acaba el espacio, como cuando escribimos texto. El tercer administrador de diseño es BoxLayout, cada componente escoge su tamaño y se ordenan todos los componentes o verticalmente u horizontalmente. Si queremos ordenar los dos botones que pusimos en la región EAST, uno encima del otro, podemos usar BoxLayout en su versión vertical si se lo especificamos al JPanel, para esto usamos el método setLayout() al cual le pasamos una nueva instancia del administrador de diseño que en este caso recibe dos parámetros, la referencia del componente y la forma de ordenar que en este caso es vertical y se especifica con la constante Y_AXIS: contenedor.setLayout(new BoxLayout(contenedor, BoxLayout.Y_AXIS)); El método setLayout() nos permite cambiar el administrador de diseño. Si agregamos esta línea a nuestro código anterior el resultado será el siguiente: 135 Aunque los administradores de diseño pueden ser muy útiles, hay veces que no queremos usar ninguno. Esto nos permite escoger el tamaño y posición exactos para cada componente. Sin embargo, debemos ser cuidadosos porque dependiendo del sistema operativo, ciertos botones pueden necesitar ser más grandes para que su contenido o su texto se muestre completamente. Es por esto que siempre que usemos componentes como JButton es mejor dejar que se acomoden usando un administrador de diseño o ser lo suficientemente generosos con el espacio al acomodarlos. Para deshabilitar los administradores de diseño para el JFrame usamos el siguiente código: ventana.setLayout(null); Luego para posicionar un componente, por ejemplo un botón, usamos el método setBounds() de la siguiente forma: boton.setBounds(10, 30, 150, 40); El primer número es la distancia en pixeles desde el borde izquierdo del contenedor, el segundo es la distancia en pixeles desde el borde superior del contenedor, el tercer número es el ancho del componente en pixeles y el cuarto número es el alto del componente en pixeles. En el siguiente código estamos posicionando dos botones de forma absoluta, esto quiere decir que nosotros escogemos tanto la posición como el ancho y el alto: import javax.swing.*; public class Main { public static void main(String[] args) { JFrame ventana = new JFrame("Regiones"); ventana.setLayout(null); JButton boton1 = new JButton("Boton 1"); 136 ventana.add(boton1); boton1.setBounds(70, 30, 150, 40); JButton boton2 = new JButton("Boton 2"); ventana.add(boton2); boton2.setBounds(70, 80, 150, 40); ventana.setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE); ventana.setSize(300, 200); ventana.setVisible(true); } } El resultado visual es el siguiente: Si lo queremos podemos agregar otro tipo de componentes como contenedores de texto, tablas, botones radiales, campos de contraseñas y muchos otros que puedes aprender a usar a fondo si buscas sobre cada uno en internet o en libros profesionales sobre el tema. Todos son muy sencillos de usar y veremos algunos otros cuando sepamos sobre manejo de eventos. Por ejemplo, podemos hacer áreas de texto con scroll. Para lograrlo usamos el objeto JTextArea para el área de texto y JScrollPane para el scroll. 137 import javax.swing.*; public class Main { public static void main(String[] args) { JFrame ventana = new JFrame("Texto"); JTextArea texto = new JTextArea("Todo el texto..."); JScrollPane scroll = new JScrollPane(texto); texto.setLineWrap(true); scroll.setVerticalScrollBarPolicy(ScrollPaneConstants.VERTICAL_ SCROLLBAR_ALWAYS); scroll.setHorizontalScrollBarPolicy(ScrollPaneConstants.HORIZONTAL _SCROLLBAR_NEVER); ventana.add(scroll); ventana.setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE); ventana.setSize(300, 300); ventana.setVisible(true); } } Las dos líneas dentro de main() que no tienen tabulación van seguidas sin dejar espacio en su línea anterior, simplemente no cupieron en una sola línea y por eso aparecen de esta forma. En azul vemos el código necesario para hacer el área de texto con scroll. el resultado del texto anterior es el siguiente: 138 La característica de las áreas de texto es que podemos escribir sobre ellas todo lo que queramos, simplemente la seleccionamos con el cursor y escribimos. si agregamos suficiente texto veremos que el scroll vertical empieza a funcionar: JTextArea tiene un constructor que recibe el texto que queremos poner dentro. JScrollPane tiene un constructor que recibe un área de texto. Luego debemos usar el método setLineWrap() que recibe un booleano para poder envolver el texto dentro de su espacio, esto es necesario para usar un scroll. Las dos siguientes líneas nos permiten crear el scroll vertical pero no permitir el scroll horizontal. Por último agregamos el JScrollPane al contenedor y no el JTextArea. En la gran mayoría de aplicaciones profesionales se usan diseños de botones, fondos y textos creados por diseñadores. En este caso la mejor opción es usar imágenes que los diseñadores nos han entregado previamente. Una forma muy útil de poner imágenes es crear una clase que extienda JPanel, luego debemos sobrescribir un método llamado paintComponent() que recibe un objeto de tipo Graphics que es llamado automáticamente por Java. Dentro de este método podemos escribir el código para usar la imagen. Luego simplemente creamos un objeto de esta clase y agregamos la referencia de la misma forma que hemos agregado los botones hasta ahora. Para que este código funcione debes buscar una imagen en tu computador, aprenderte la ruta de la misma y el nombre con su extensión. Por ejemplo yo voy a usar la siguiente imagen, que se encuentra en 139 D:/images/xxx.jpg como puedes ver por la ruta, el nombre de la imagen es xxx.jpg. El tamaño es de 200 por 200 pixeles: El siguiente es el código completo para ver la imagen en una aplicación Java: import javax.swing.*; import java.awt.*; public class Main{ public static void main(String[] args) { JFrame ventana = new JFrame("Imágenes"); ventana.setLayout(null); Pintar pintar = new Pintar(); ventana.add(pintar); pintar.setBounds(2, 1, 200, 200); ventana.setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE); ventana.setSize(220, 240); ventana.setVisible(true); } } class Pintar extends JPanel { public void paintComponent(Graphics g) { Image imagen = new ImageIcon("D:/ images/xxx.jpg").getImage(); g.drawImage(imagen,0,0,this); 140 } } El resultado es el siguiente: Si analizas el código verás que no es nada complejo. Simplemente tenemos una clase que llamamos Pintar, la cual sobrescribe un método de Java llamado paintComponent() que recibe un objeto Graphics. Este método no tenemos que llamarlo ya que Java lo llama automáticamente por nosotros. Dentro del método usamos una subclase de Image llamada ImageIcon que nos permite llamar la imagen escribiendo la ruta de la misma y luego usando su método getImage(). Luego usamos el parámetro de Graphics para usar el método drawImage() que es el encargado de recibir cuatro parámetros para mostrar la imagen. Primero recibe la referencia de la imagen, luego las coordenadas 'x' y 'y' en pixeles, por último usamos la palabra clave this para referirse a esa misma clase que es la que extiende JPanel. En main() estamos creando un objeto de esta clase y lo agregamos como hemos hecho con todos los componentes. Sin embargo, tenemos un problema con nuestro código anterior. Cuando le entreguemos a alguien la aplicación que ellos van a ejecutar, esto quiere decir el archivo JAR que creamos con el botón 'Clean and build project' en NetBeans, ellos no van a tener la imagen en la ruta que especificamos en el código en su 141 computador. Podríamos darles la imagen y pedirles que la guarden en la ubicación correcta pero esto no sería práctico, además una persona en Mac o Linux no usa la misma estructura de archivos empezando por D:, incluso en PC la persona puede no tener una partición del disco duro llamada D. Es importante entender que en el mundo de la programación existen dos formas principales de escribir rutas de archivos externos. Con archivos externos me refiero por ejemplo a la imagen que estamos usando en el ejemplo anterior. Existen rutas absolutas y rutas relativas. Las rutas absolutas son aquellas en las que especificamos la dirección del archivo desde la raíz del disco duro o en ocasiones desde la raíz de una dirección URL que son las que se usan en internet. En nuestro ejemplo anterior usamos una ruta absoluta ya que especificamos su ubicación desde D:. Como ejemplo de rutas absolutas encontramos D:/images/xxx.jpg o en URLs encontramos http://ladomicilio.com/images/xxx.jpg. Las rutas relativas, como su nombre lo indica, son rutas que especificamos de forma relativa al documento donde escribimos la dirección. Por ejemplo en el caso de la imagen de nuestro ejemplo anterior, pudimos escribir en la ruta del archivo simplemente 'xxx.jpg', esto quiere decir busca el archivo xxx.jpg en la misma carpeta en la que está el archivo JAR, en este caso debemos asegurarnos que nuestro archivo JAR, que es el que debemos entregar a las personas, esté siempre acompañado de la imagen llamada xxx.jpg en la misma carpeta. El archivo JAR al crearlo desde NetBeans queda guardado dentro de la carpeta dist, es allí donde debemos poner también nuestra imagen si vamos a especificar una ruta relativa. Si tenemos muchas imágenes podemos crear una carpeta llamada images en la que guardamos todas las imágenes, si colocamos esta carpeta en la misma ubicación del archivo JAR, para llamar la imagen de forma relativa usamos 'images/xxx.jpg'. El separador / se usa para especificar el contenido de una carpeta. Si quisiéramos devolvernos una carpeta usaríamos el código '../' que son dos puntos seguidos y un slash. 142 Si bien las rutas relativas son la mejor opción, todavía deberíamos entregar al usuario no sólo el archivo JAR sino también la carpeta llamada images. En realidad la mejor opción es agregar nuestras imágenes al archivo JAR. Recordemos que los archivos .jar funcionan como los ZIP, esto quiere decir que empaquetan varios archivos en uno solo. Por lo tanto también podemos agregar imágenes y otros archivos dentro del JAR, así sólo tenemos que entregar este único archivo al cliente. Para agregar imágenes a NetBeans e incrustarlas en el JAR, primero debemos crear una nueva carpeta con el nombre que queramos, en este caso la voy a llamar images, si este fuera un proyecto grande, aquí pondríamos todas las imágenes. Esta carpeta debe ir en la carpeta llamada src del proyecto, aunque en la ventana projects de NetBeans aparece como Source Packages. Podemos crear esta carpeta manualmente o desde NetBeans haciendo clic derecho sobre Source Packages > New > Folder, allí especificamos el nombre y hacemos clic en Finish. A esta carpeta podemos arrastrar nuestra imagen. Al final debemos tener lo siguiente en las pestañas Projects y Files: La ventana Projects en NetBeans no muestra exactamente la organización de archivos en el computador del proyecto, simplemente muestra una organización interna de nuestro proyecto. Para ver la organización de archivos en nuestro computador usamos la pestaña Files: 143 Con nuestro archivo en su carpeta correcta, podemos llamarlo cambiando el código del objeto ImageIcon. Antes pasamos a su constructor un String con la ruta relativa o absoluta del archivo. Ahora debemos pasar lo siguiente: ImageIcon(getClass().getResource("images/xxx.jpg")) La línea completa de nuestro código original era: Image imagen = new ImageIcon("D:/ images/xxx.jpg").getImage(); La línea debe quedar así para poder leer la imagen desde el archivo JAR: Image imagen = new ImageIcon(getClass().getResource("images/xxx.jpg")).getImage(); Al construir nuestro proyecto obtendremos un archivo JAR que contiene nuestras imágenes. Los métodos getClass() y getResource() que recibe la ruta relativa de la imagen desde la carpeta src, son necesarios para leer archivos contenidos en el JAR. Por último, escribe el siguiente código reemplazando el contenido de paintComponent() y mira que Java también es capaz de crear figuras geométricas 144 por nosotros, esto es muy útil cuando los diseñadores o nosotros mismos queremos algo básico en pantalla y no podemos darnos el lujo de poner tantas imágenes ya que esto terminaría afectando el rendimiento de la misma: g.setColor(Color.ORANGE); g.fillRect(0,0,200,200); Este código nos crea un cuadrado de 200 por 200 pixeles en pantalla de color naranja como muestra la siguiente imagen: Puedes cambiar el color cambiando ORANGE por casi cualquier otro nombre de color que se te ocurra en inglés. Los cuatro parámetros de fillRect() son la coordenadas desde 'x' y 'y', luego el ancho y el alto. Puedes buscar en el API de Graphics, allí encontrarás métodos para crear polígonos, óvalos y otras figuras geométricas. Para crear animaciones o si en algún momento de tu aplicación quieres volver a llamar el método paintComponent(), simplemente debes escribir ventana.repaint(); recordemos que ventana es la referencia al JFrame. Aunque este tema es demasiado extenso y aquí apenas puedo tocar nociones muy básicas, ya podemos empezar a comunicarnos con los usuarios de nuestras aplicaciones. En el siguiente capítulo aprenderemos cómo hacer para que los usuarios puedan interactuar con los GUI. 145 Eventos Los eventos nos permiten interactuar con nuestros GUI y con las aplicaciones. Los eventos no son más que porciones de código que se ejecutan cuando una situación particular ocurre, como por ejemplo cuando un usuario hace clic sobre un botón. Java nos permite manejar eventos de varias formas, sin embargo, voy a enfocarme sólo en la forma más robusta. Para esto necesitamos entender primero el concepto de clases internas, esto quiere decir una clase dentro de otra clase. La forma simple de una clase interna es la siguiente: class Externa { // código clase externa class Interna { // código clase interna } } No debemos confundir este concepto de clases internas con el concepto que teníamos de antes de clases externas cuando nos referíamos a clases que se encontraban en otros archivos. Aquí simplemente estamos hablando de una clase dentro del bloque de otra clase. Este tipo de clases internas pueden acceder tanto a los métodos como a las variables así sean private. En el ejemplo anterior tenemos una clase llamada Externa que tiene dentro una clase interna llamada Interna. Dentro de la clase madre externa podemos crear una referencia u objeto de la clase interna de la misma forma que se crea cualquier objeto. Este tipo de clases son muy útiles porque es en una clase interna donde ponemos todo el código que queremos ejecutar cuando ocurre un evento. Supongamos que queremos que un botón nos borre el contenido de un área de texto. En el capítulo anterior aprendimos a crear áreas de texto con scroll y también aprendimos a crear botones, por lo tanto no me detendré en la porción del código que crea el 146 GUI, en azul vemos el código necesario para crear el evento que borra el contenido del área de texto: import javax.swing.*; import java.awt.BorderLayout; import java.awt.event.*; public class Main { JTextArea texto; public static void main(String[] args) { Main main = new Main(); main.gui(); } public void gui(){ JFrame ventana = new JFrame("Eventos"); texto = new JTextArea("Este es el texto que vamos a \nborrar"); texto.setLineWrap(true); ventana.add(BorderLayout.CENTER, texto); JButton boton = new JButton("BORRAR TODO"); ventana.add(BorderLayout.SOUTH, boton); boton.addActionListener(new EventoBoton()); ventana.setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE); ventana.setSize(200, 200); ventana.setVisible(true); } class EventoBoton implements ActionListener { public void actionPerformed(ActionEvent event) { texto.setText(""); } } } 147 El resultado es el siguiente: Si presionamos el botón que dice "BORRAR TODO", veremos cómo el texto desaparece. Analicemos el código en azul que es el encargado del evento. El resto del código lo puedes analizar por tu cuenta, aunque cabe destacar que no estamos generando todo el código desde main(), allí simplemente estamos creando un objeto de la clase Main para poder llamar el método gui(). Recordemos que no podemos llamar gui() directamente desde main() sin crear un objeto porque éste es un método static, lo que no le permite acceder a otros métodos no estáticos dentro de la misma clase. También cabe notar que hemos creado la variable texto fuera de todo método, esto nos permite llamarlo desde el método gui() y desde la clase interna EventoBoton que maneja el evento. Si no hubiéramos hecho esto, no podríamos acceder a la misma variable desde ambas partes debido a los ámbitos locales, capítulo que puedes repasar si has olvidado algo al respecto. Observa también que dentro del texto de JTextArea agregamos \n que significa salto de línea, esto es lo que nos permite simular un enter. El primer código azul que encontramos es lo primero que debemos hacer siempre para usar los eventos, esto es importar el paquete java.awt.event ya que sin éste no podemos usar los eventos. El siguiente código azul que encontramos está dentro del método gui() y es la línea boton.addActionListener(new EventoBoton()); que es la encargada de agregarle el evento al botón correspondiente. El método 148 addActionListener() es una forma de decir que le estamos agregando un evento a boton. A este método le estamos pasando un argumento que es una nueva instancia de la clase interna que hemos llamado EventoBoton pero puedes llamarla como quieras. Con esta línea, cada vez que se presione el botón, se creará una nueva instancia de la clase interna. El siguiente código azul que encontramos es la clase interna. Ésta puede llamarse de cualquier forma pero debe implementar ActionListener. Recordemos que al implementar una interfaz debemos sobrescribir sus métodos, el único método que tiene ActionListener es actionPerformed() que recibe un objeto ActionEvent. Entonces en la clase interna debemos implementar ActionListener y debemos sobrescribir actionPerformed(), en este método escribimos el código que va a ocurrir cuando presionamos el botón que dispara este evento. En este caso todo lo que hace el botón es texto.setText(""); que no es más que poner en blanco el JTextArea igualando su contenido a unas comillas vacías de contenido. Con este código hemos creado nuestro primer evento. Si tuviéramos más de un botón simplemente crearíamos una clase interna para cada uno de los eventos y a cada botón le asignaríamos un método addActionListener() con su respectiva nueva clase interna. También existen otros tipos de eventos, más adelante cuando hablemos sobre MIDI y audio, veremos que también existen ciertos tipos de eventos relacionados a ellos. Aparte de hacer clic sobre un botón, muchas veces también nos interesa saber cuando un usuario está usando el teclado del computador. El siguiente es el código completo para una aplicación muy simple en la que cada vez que presionamos una tecla, el título del JFrame se cambia a la letra, número o código presionado. Si presionamos una letra o número, éste aparece en el título, al soltar la tecla el título se cambia a 'Soltaste la tecla.'. En caso de presionar una flecha o una tecla como f1 o f2, obtenemos un código específico de esas teclas. En azul vemos el código específico encargado de manejar los eventos de teclado: 149 import javax.swing.*; import java.awt.event.*; public class Main { JFrame ventana; public static void main(String[] args) { Main main = new Main(); main.gui(); } public void gui(){ ventana = new JFrame("Eventos"); JTextArea texto = new JTextArea("Escribe aquí..."); texto.setLineWrap(true); ventana.add(texto); texto.addKeyListener(new EventoTeclado()); ventana.setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE); ventana.setSize(300, 200); ventana.setVisible(true); } class EventoTeclado implements KeyListener { public void keyPressed(KeyEvent event) { ventana.setTitle("" + event.getKeyCode()); } public void keyReleased(KeyEvent event) { ventana.setTitle("Soltaste la tecla."); } public void keyTyped(KeyEvent event) { ventana.setTitle("" + event.getKeyChar()); } } } 150 Igual que con los botones debemos primero importar el paquete java.awt.event. Además agregamos el método addKeyListener() al componente de texto, este método recibe una nueva instancia de la clase interna. Esta clase debe implementar la interfaz KeyListener, que tiene tres métodos: keyPressed(), keyReleased() y keyTyped(), los tres reciben un argumento del tipo KeyEvent. keyReleased() se dispara cuando un usuario ha soltado una tecla que había presionado. keyTyped() funciona para letras y números en el teclado. keyPressed() se usa para teclas como flechas y otras como f1, f2, etc. En cada uno de estos métodos usamos ventana.setTitle() para cambiar el título del JFrame. Usamos el parámetro de tipo KeyEvent para obtener la tecla presionada. event.getKeyChar() sólo funciona para letras y números y sólo se usa en keyTyped(). event.getKeyCode() funciona para obtener los códigos por ejemplo de las flechas, donde arriba es '38' y abajo es '40'. De esta forma podemos usar una sentencia de prueba if para saber si el usuario presionó la flecha hacia arriba. En definitiva la aplicación anterior nos sirve para probar cuál es el resultado que recibe el programa cuando presionamos y soltamos una tecla. Si mantenemos presionada la tecla 'a' veremos en el título la tecla 'a'. Al soltarla veremos que el título cambia. Al mantener una tecla presionada como las flechas, veremos su código en el título. Más que ser una aplicación útil, nos demuestra cómo usar los eventos de teclas y sobre todo nos muestra lo parecido que es usar los eventos sin importar que sean botones o teclas. Existen eventos para el movimiento del mouse, para todos los botones del mismo, para eventos MIDI, cuando un objeto es modificado, y muchos otros. Lo más importante es entender la importancia de las clases internas en el proceso de los eventos. Cuando necesites entender muchos otros tipos de eventos, simplemente puedes ir y buscar el API para dicho evento y con estas bases podrás aprender a registrar el evento que buscas. 151 Números binarios, decimales y qué es MIDI El mundo del MIDI es muy amplio y no pretendo abarcarlo todo aquí. La misión de estos primeros capítulos es dar un vistazo general, como especie de repaso, que te permite entender de forma general qué es MIDI y su funcionamiento básico para que puedas crear aplicaciones en Java. Entre más conozcas sobre este mundo, mejores y más diversas aplicaciones podrás crear. Si por un momento creímos que al llegar a la parte de MIDI y audio las siglas iban a terminar, pues no. MIDI significa Musical Instrument Digital Interface y se creó por necesidad en la comunicación entre diversos aparatos electrónicos musicales de diferentes marcas. Con la aparición de los sintetizadores, varias compañías empezaron a fabricar diferentes aparatos, cada uno con sus ventajas y con sonidos mejores que otros, esto llevó a que los músicos quisieran poder tocar más de un sintetizador a la vez y por lo tanto la necesidad de un protocolo de comunicación estándar entre las máquinas se hizo evidente. Este protocolo apareció entre 1982 y 1983, desde ese entonces casi todos los aparatos musicales electrónicos han adoptado el MIDI como forma de comunicarse con el mundo externo. Obviamente las dos partes que van a comunicarse deben poder manejar MIDI, con una sola no basta. El protocolo MIDI se basa en comunicación digital, esto quiere decir que en su nivel más básico estamos hablando de números representados por unos y ceros. Cada uno de estos unos y ceros es llamado un bit. El conjunto de 8 bits se ha denominado un byte. En un byte podemos escribir alguno de 256 valores, que para nuestro caso son los valores entre 0 y 255: 0 = 00000000 1 = 00000001 2 = 00000010 3 = 00000011 152 4 = 00000100 5 = 00000101 6 = 00000110 7 = 00000111 8 = 00001000 9 = 00001001 Como podemos ver, cada vez que se aumenta en uno el número decimal, un cero del número binario debe convertirse en uno, empezando de derecha a izquierda, pero los unos que le sigan, si los hay, deben convertirse en cero. Siempre hasta llenar una posición de solo unos para poder pasar al siguiente nivel hacia la izquierda. Aunque esta sea la forma lógica de entenderlo, siempre es mucho más práctico aprender a hacer conversiones entre decimales y binarios. Para pasar un número decimal a binario simplemente dividimos entre dos hasta llegar a uno. Si queremos convertir el número 144 a binario procedemos de la siguiente forma: 144 = 10010000 Primero empezamos por el número 144 que debemos dividir siempre entre 2 como muestran los números azules. Paramos la división cuando lleguemos a uno. En rojo escribimos el residuo de la operación. El resultado de cada división lo podemos ver en verde, aunque el último resultado lo marcamos rojo porque lo 153 vamos a usar como el primer valor de nuestro número binario. 144 dividido 2 nos da 72 y el residuo es cero. 72 dividido 2 es 36 y el residuo es cero. 36 dividido 2 es 18 y el residuo es cero. 18 dividido 2 es 9 y el residuo es cero. 9 dividido 2 es exactamente 4.5 pero lo tratamos como una división de enteros y escribimos 4 en el resultado y 1 en el residuo. 4 dividido 2 es 2 y el residuo es 0. Por último 2 dividido 2 es 1 y el residuo es 0. Al final simplemente debemos ordenar el número binario que está dado por los residuos o números rojos pero al revés, de derecha a izquierda. Si por ejemplo queremos convertir el número 13 a binario simplemente procedemos de la siguiente forma: 13 = 1001 Como vemos, en este caso obtenemos un valor de sólo 4 bits. Para convertirlo a valores de 8 bits simplemente agregamos ceros a la izquierda hasta completar 8 bits: 13 = 00001001 Para convertir desde un número binario a uno decimal, lo único que debemos hacer es multiplicar por dos empezando desde el número 1, teniendo en cuenta que cuando haya un uno en la sección binaria, debemos sumar uno a la 154 multiplicación por dos. Si queremos pasar a decimal el número binario 1001000 procedemos de la siguiente forma: 1001000 = 144 Lo primero que hacemos es ordenar el número binario al lado izquierdo en forma vertical. El primer número 1 binario siempre va a ser igual al número uno decimal. A partir de ahí empezamos a multiplicar por dos el último número decimal que tengamos, pero siempre que nos encontremos un uno en la parte izquierda debemos sumarle uno a la multiplicación. Es muy importante aprender a hacer este tipo de conversiones ya que Java maneja el MIDI directamente desde su nivel más bajo, esto quiere decir desde los valores binarios. Por ejemplo cuando vamos a decirle a un sintetizador que haga sonar una nota, uno de los valores que necesita Java es 1001000 que es el valor estándar determinado para MIDI que dice que se debe encender una nota. Existen 256 tipos de mensajes MIDI y es muy probable que para conocerlos todos termines accediendo a tablas que encuentres en internet o en libros profesionales sobre el tema. Estas tablas pueden llegar a mostrar los números binarios que representan cada valor, pero los mensajes MIDI en Java deben escribirse en decimal. Imagina que un día estás creando una aplicación MIDI en la que quieres que el usuario final pueda cambiar el tono de una nota y descubres que el mensaje MIDI 155 que te permite usar el Pitch Bend es el número binario 11100000, si Java está esperando el valor decimal de este número, ¿cómo haces para convertir de binario a decimal? La primera opción es usar las operaciones que te enseñé anteriormente. La segunda opción es usar Java. Convertir de binario a decimal en Java es muy sencillo, simplemente usamos el método estático parseInt() de la clase Integer que se encuentra en java.lang, por lo tanto no tenemos que importarla, Para hacer esta conversión debemos pasarle al método dos argumentos, el primero es un String con el número binario, el segundo es el número 2 que significa que estamos haciendo una conversión en base dos necesaria para hacer la conversión que queremos. int decimal = Integer.parseInt("11100000", 2); En este caso, la variable llamada decimal va a ser 224 que es el número decimal correcto del binario 11100000. Si por el contrario quieres transformar de un número decimal a uno binario, usamos el método estático toString() de la clase Integer. Este método también recibe dos argumentos, primero el número decimal que queremos convertir y luego la base de la conversión que sigue siendo 2. String binario = Integer.toString(224, 2); En este caso, la variable binario es igual a 11100000. Ya entendiendo los números binarios, su relación con los decimales y sabiendo cómo convertir entre ellos, podemos volver a lo fundamental, tratar de entender qué es MIDI y cómo funciona. Hasta ahora sólo sabemos que MIDI es un protocolo binario que usan muchos instrumentos musicales para comunicarse entre sí. El MIDI se ha hecho tan famoso que hoy día no sólo se ve entre instrumentos musicales, muchos aparatos de audio profesional y algunos aparatos de luces también son compatibles. Para entender el MIDI debemos empezar a analizar cómo se transmiten los bits entre las máquinas. 156 La comunicación MIDI Entender la forma en que la información MIDI es transmitida entre equipos es indispensable para conocer sus límites. Si hemos dicho que el MIDI no es más que un protocolo binario de comunicación entre aparatos que adapten esta tecnología, debemos saber la forma en que viaja la información. No está de más dejar claro que a través de MIDI NO se envían sonidos ni música como tal, más bien se envía la información necesaria que le permite a los dispositivos hacer música. Esto funciona como una especie de partitura que de por sí no es música, sino una representación de la música misma para que alguien la pueda interpretar. Ahora, MIDI no sólo funciona para transmitir mensajes musicales, también es muy útil para manipular ciertas funciones de un aparato a través de un segundo aparato. La información MIDI viaja de forma serial. Existe tanto la transmisión paralela como la transmisión serial. En la paralela, se pueden enviar bits de información al mismo tiempo, esto se logra usando varios cables donde cada uno lleva parte de la información y al final el resultado es la llegada de varios bits de información simultáneamente. En la transmisión serial estamos usando un único cable para enviar la información, esto limita el envío a una fila de bits. Un bit va justo detrás del otro, esto implica menor transmisión de data, pero también implica una disminución en los costos. Con toda transmisión serial, necesitamos que la velocidad sea lo suficientemente rápida para que las aplicaciones más demandantes puedan funcionar sin verse afectadas por la fila de bits que es enviada. Imaginemos que estamos tocando en vivo un controlador6 que a través de MIDI usa los sonidos de un programa del computador. Producir una nota y luego 6 Un controlador es un dispositivo que no genera sonidos por sí mismo, su función es enviar información de una ejecución musical o información de otro tipo que luego será procesada por otro dispositivo capaz de hacer algo con dicha información, ya sea generar sonidos o disparar funciones específicas. Existen teclados en el mercado que no producen ningún sonido, simplemente envían información MIDI. 157 apagarla le toma al MIDI 6 bytes de información, recordemos que un byte son 8 bits. Si estamos tocando muchas notas y de pronto en un momento crucial de la canción necesitamos cambiar el efecto en nuestro sonido inmediatamente, al pensar que podríamos haber tocado 8 notas, esto quiere decir 48 bytes o 384 bits, más la información que le sigue que nos va a cambiar el efecto, si la transmisión serial no es capaz de enviar al menos 400 bits tan rápido que nuestro oído no oiga la diferencia en tiempo, entonces el MIDI no sirve para nada. Estos cálculos son sin contar que por cada byte de información, el MIDI usa dos bits extra para un total de 10 bits por mensaje, estos dos bits se usan con fines de sincronismo. Afortunadamente el MIDI si es lo suficientemente rápido incluso para otras aplicaciones más demandantes: The MIDI message for playing a single note has three bytes in it. At the speed of 31,250 bits per second, it will take .96 milliseconds to send the command to play a note from one instrument to another. To keep things simple, this number is rounded off and called one millisecond. It takes another three bytes-another millisecond-to shut that note off. If it takes one millisecond to turn the note on and one to turn the note off, then MIDI can play approximately 500 notes a second-a lot of notes! (Rona, 1994:14) Como bien lo dice el párrafo anterior, la velocidad de envío del MIDI es 31250 bits por segundo. Aunque es un número demasiado grande, debe tenerse en cuenta cuando estemos haciendo aplicaciones demasiado demandantes. La información MIDI entre dispositivos, es enviada normalmente a través de cables MIDI, éstos usan los conectores DIN que tienen 5 pins en cada punta, éstos pueden usar hasta 5 cables internos, pero sólo uno es usado para enviar la información MIDI, el resto son protección, tierra y otros dos que normalmente no se usan. Hoy día también es muy común ver cables USB para conectar dispositivos y enviar información MIDI, se ven mucho en los teclados para poder conectarlos al computador, lo bueno es que todo el mundo tiene un puerto USB en 158 su computador, en cambio no todos tienen interfaces MIDI que permiten conectar cables MIDI. Así se ve el conector DIN: Todos los puertos siempre serán hembras y los cables en sus dos extremos siempre son machos. Debemos ser cuidadosos con el largo de los cables MIDI ya que esto afecta la integridad de los datos. Entre más largo sea un cable MIDI, hay más posibilidades de transformación de la información original. Estos conectores MIDI se usan con tres tipos principales de puertos: El puerto OUT o de salida se usa para enviar información desde el dispositivo que lo tiene. El puerto IN o de entrada sirve como receptor de la información MIDI, proveniente de otros dispositivos. EL puerto THRU es una copia exacta de la información que viene hacia el puerto IN, si no entra nada por IN nada saldrá por THRU, esto con el fin de hacer cadenas de varios instrumentos conectados, por ejemplo si estamos controlando varios módulos de sonido desde un teclado que tiene una única salida MIDI, entonces podemos ayudarnos del puerto THRU de un módulo de sonido para comunicarnos con otros dispositivos a la vez. Entender las conexiones físicas y la forma en que es enviada la información MIDI es fundamental para comunicarnos con el mundo externo desde nuestras aplicaciones. Java es capaz de recibir, producir y enviar información MIDI, pero para comunicarse con el mundo externo depende de los dispositivos que estén 159 conectados al computador que ejecuta la aplicación. Por lo tanto una interfaz MIDI, tarjetas de sonido con entradas y/o salidas MIDI o dispositivos con conexión USB serán necesarios si queremos que nuestra aplicación se comunique vía MIDI con el mundo externo. Java es capaz de reconocer automáticamente, qué puertos o dispositivos están disponibles para ser usados para cada aplicación. Es importante que nuestras aplicaciones sean lo suficientemente flexibles para poder funcionar haya o no un dispositivo o puerto MIDI disponible. Imaginemos la cantidad de posibles situaciones o escenarios que podrían haber en diferentes entornos. Por ejemplo alguien en su laptop podría no tener ningún dispositivo conectado y así y todo queremos que nuestra aplicación MIDI funcione y no falle, pero al mismo tiempo un usuario podría tener 5 teclados conectados a su computador, debemos darle la posibilidad al mismo de decidir cuál quiere usar. Si queremos saber los dispositivos, programas y puertos MIDI disponibles en el computador, usamos el siguiente código: import javax.sound.midi.*; public class LearningMidi { public static void main(String[] args) { MidiDevice.Info[] dispositivos = MidiSystem.getMidiDeviceInfo(); for (MidiDevice.Info info: dispositivos) { System.out.println(info.getName()); } } } Recordemos que para usar los API de sonido y MIDI debemos primero importar los paquetes correctos. Para usar el API de MIDI importamos el paquete javax.sound.midi con todas sus clases ya que en aplicaciones reales 160 probablemente usemos varias de éstas. Dentro de main() tenemos todo el código necesario para ver en la ventana de salida los dispositivos relacionados con MIDI. MidiDevice.Info es una clase interna de MidiDevice, se usa para obtener la información general de los dispositivos MIDI encontrados en el sistema. En la primera línea de main() estamos creando un arreglo de MidiDevice.Info que hemos llamado dispositivos. Para que este arreglo se llene con los dispositivos MIDI encontrados en el sistema, debemos igualarlo a MidiSystem.getMidiDeviceInfo(). MidiSystem es una clase muy útil en diferentes momentos de la programación de aplicaciones MIDI, en este caso usamos su método estático getMidiDeviceInfo(), que según el API dice que sirve para obtener un arreglo de objetos de tipo MidiDevice.Info con la información de todos los dispositivos MIDI disponibles en el sistema. Luego hacemos un ciclo sobre el arreglo para poder usar el método getName() de MidiDevice.Info para cada uno de los ítems. En mi computador tengo una Mbox 2 pro, la tarjeta de sonido que venía con el computador es una Realtek y mi sistema operativo es Windows 7. El resultado con este sistema en la ventana de salida es: En este caso obtenemos el nombre de cada dispositivo. Tengo a mi disposición un controlador M-AUDIO Keystation 88ES, que se conecta al computador vía USB. 161 Cuando lo enciendo y vuelvo a ejecutar el programa, ahora obtengo el siguiente resultado: Como podemos ver, ahora aparece la información del controlador. Seguramente te estás preguntando cómo podemos hacer para saber qué función MIDI tiene cada uno de los ítems en la lista, incluso debes preguntarte por qué hay elementos repetidos. Para responder esta pregunta primero debemos entender los 4 tipos de recursos MIDI básicos con los que trabaja Java. Estos son los sintetizadores, los secuenciadores, los transmisores y los receptores. Un sintetizador es un objeto que implementa la interfaz Synthesizer, éstos tienen la capacidad de producir sonidos y pueden ser físicos o no. Como podemos ver en la lista de recursos de mi sistema, Java tiene su propio sintetizador, nombrado en la lista como 'Java Sound Synthesizer'. Un secuenciador es un programa, físico o no, capaz de reproducir secuencias. Recordemos que una secuencia no es más que una serie de información MIDI con estampillas de tiempo para cada uno de los eventos. Un secuenciador implementa la interfaz Sequencer. Un transmisor es un objeto de tipo Transmitter que emite mensajes MIDI. Por ejemplo el controlador 'USB Keystation 88es' y en general todos los controladores deben convertirse en objetos de tipo Transmitter para poder ser usados dentro de una aplicación Java 162 ya que son aparatos que transmiten información MIDI. Un receptor es todo lo contrario a un transmisor ya que su función es recibir información MIDI. Éste es un objeto de tipo Receiver y puede pensarse como el puerto MIDI-IN de la aplicación. Por ejemplo un transmisor como el controlador M-AUDIO debe conectarse a un receptor para poder funcionar, sin el receptor no habría nadie que oyera la información que esté enviando el transmisor. Una buena opción para empezar a entender la lista que nos provee MidiSystem.getMidiDeviceInfo(), es saber a cuáles de los elementos se les pueden crear transmisores y a cuáles receptores. Para lograrlo, debemos obtener cada elemento usando el método getMidiDevice(), el cual recibe un objeto de tipo MidiDevice.Info que son los objetos que obtenemos en el arreglo. Con esto estamos obteniendo el dispositivo para la aplicación pero no lo estamos usando todavía porque es como si estuviera apagado para Java, para encenderlo usamos el método open() de MidiDevice. Siempre que queramos usar uno de los elementos de la lista, lo escogemos con su número de índice de arreglo y luego usamos open(). Si quisiéramos usar el primer elemento de la lista, usamos el siguiente código: MidiDevice aparato = MidiSystem.getMidiDevice(dispositivos[0]); aparato.open(); Si hemos abierto un aparato con el método open(), debemos asegurarnos de cerrarlo en el momento que no se use más para liberarlo de los recursos del sistema mediante el método close(). Luego de obtener un aparato, nos ayudamos de los siguientes métodos de MidiDevice que nos permiten saber la cantidad máxima de receptores y/o transmisores que se pueden crear para un elemento: getMaxReceivers() y getMaxTransmitters(). Estos métodos devuelven 0 cuando no se puede crear su 163 tipo de objeto, devuelven -1 cuando se puede crear ilimitado7 número de transmisores o receptores, o también pueden devolver el número exacto de transmisores o receptores que pueden ser creados. Para obtener el siguiente resultado, que nos permite saber si a un elemento de la lista se le pueden crear transmisores o receptores: 7 En realidad no se pueden crear ilimitado número de transmisores o receptores, esto depende de la memoria disponible en el sistema. La cantidad exacta no se puede determinar y varía entre diferentes ambientes. En general podemos crear más de un receptor para un mismo controlador si así lo queremos, lo importante es que un receptor debe ir con un solo transmisor y viceversa. 164 Aquí podemos ver que los elementos de la lista que se llaman igual, tienen en realidad diferentes capacidades de transmisores y receptores. Esto ocurre por lo general porque uno es una entrada y el otro es una salida. Para el resultado anterior usamos el siguiente código: import javax.sound.midi.*; public class Main { public static void main(String[] args) { MidiDevice.Info[] dispositivos = MidiSystem.getMidiDeviceInfo(); for (MidiDevice.Info info: dispositivos) { System.out.println(info.getName()); try { MidiDevice aparato = MidiSystem.getMidiDevice(info); System.out.println("Transmisores: "+aparato.getMaxTransmitters()); System.out.println("Receptores: "+ aparato.getMaxReceivers()); }catch (Exception e) { System.out.println(e); } } } } El contenido dentro del ciclo en el código anterior debe ser ordenado visualmente usando tabulación, por cuestiones de espacio lo he omitido. Como podemos ver, el programa es muy similar al código anterior que nos muestra la lista de recursos MIDI disponibles. El código nuevo aparece desde el manejo de la excepción. Muchos de los códigos sobre sonido y MIDI en Java arrojan excepciones. Esto ocurre porque muchos de sus métodos pueden no darse por múltiples razones. Debemos ser conscientes que aunque tengamos una lista con los recursos MIDI, 165 esto no nos asegura que otra aplicación los esté usando. Recordemos que cada vez que un método arroja una excepción, debemos usar dicho método en un trycatch. Revisando el API de MIDI de Java, el método getMidiDevice() arroja un MidiUnavailableException y un IllegalArgumentException, para capturarlos ambos usamos de forma polimórfica el objeto Exception, ya que recordemos que toda excepción extiende este objeto. Las dos siguientes líneas que escriben en la ventana de salida, las dejé dentro del bloque try para poder usar la variable aparato, ya que fuera no se puede usar debido al ámbito local. Este código nos permite empezar a buscar en la lista, si a un elemento se le pueden crear receptores, esto quiere decir que a este elemento se le puede enviar información. Si a un elemento se le pueden crear transmisores, esto quiere decir que este ítem envía información. Por lo general a los secuenciadores se les pueden crear tanto receptores como transmisores. Con el código anterior obtenemos únicamente transmisores y receptores, pero recordemos que hay cuatro tipos principales de objetos que gobiernan el MIDI en Java. Para saber cuáles son sintetizadores y cuáles son secuenciadores agregamos el siguiente código al final del bloque de try: if(aparato instanceof Synthesizer) { System.out.println("Sintetizador"); } else if (aparato instanceof Sequencer) { System.out.println("Secuenciador"); } Este código imprime la palabra 'Sintetizador' o 'Secuenciador' cuando un elemento de la lista puede ser un objeto de alguno de estos dos tipos. Con este código agregado al anterior ya podemos saber qué podemos hacer sobre cada uno de los elementos de la lista. La palabra instanceof entre una variable y un objeto devuelve true cuando la variable es del tipo del objeto. Por ejemplo voy a usar el controlador M-Audio para enviar MIDI al sintetizador de Java. Para esto debemos 166 mirar la lista y darnos cuenta que el controlador aparece dos veces, pero como el controlador va a transmitir y no a recibir, debemos usar el que nos permite crear un Transmitter que es el número 3 en la lista, pero como es un arreglo debemos usar su índice que es el número 2. El número de índice de arreglo para el sintetizador es el número 9. Con el siguiente código usamos el método getMidiDevice() para obtener tanto el M-AUDIO como el sintetizador usando sus índices de arreglo de la variable dispositivos. El siguiente código abre los aparatos para poder usarlos pero no los cierra, esto es un error en aplicaciones, pero para el ejemplo vamos a dejarlo así: import javax.sound.midi.*; public class LearningMidi { public static void main(String[] args) { MidiDevice.Info[] dispositivos = MidiSystem.getMidiDeviceInfo(); try { MidiDevice aparato = MidiSystem.getMidiDevice(dispositivos[2]); aparato.open(); Transmitter maudio = aparato.getTransmitter(); Synthesizer sintetizador = (Synthesizer) MidiSystem.getMidiDevice(dispositivos[9]); sintetizador.open(); Receiver receptor = sintetizador.getReceiver(); maudio.setReceiver(receptor); } catch (Exception e) { System.out.println(e); } } } 167 El código anterior debería tener tabulación dentro del bloque try, por cuestiones de espacio lo dejé así. La línea 10 debería estar en la línea anterior luego del cast, por la misma razón de espacio la dejo allí. Este cast debe hacerse porque getMidiDevice() devuelve un objeto de tipo MidiDevice que es una superclase de Synthesizer, y también de la clase Sequencer. Por eso cuando necesitamos un secuenciador o un sintetizador debemos hacer un cast. La variable maudio contiene la referencia al Transmitter del controlador. La variable sintetizador hace referencia al sintetizador, como éstos son subclases de MidiDevice, pueden usar sus métodos, en este caso necesitamos getReceiver() para obtener un receptor que hemos puesto en la variable receptor que se puede conectar con el transmisor del controlador. Para conectar un transmisor con un receptor, usamos el método setReceiver() de Transmitter, que recibe como argumento un objeto de tipo Receiver. Al correr el programa podemos tocar el controlador y sonará el sintetizador, por no usar close() en los dispositivos, nuestra aplicación no termina nunca, para detenerla debemos usar el botón Stop de la ventana de salida. No siempre es necesario escoger recursos de la lista que nos provee el sistema, podemos escribir programas de tal forma que seleccione de forma automática los recursos predeterminados. Para lograrlo simplemente nos valemos de los métodos de MidiSystem. Recordemos que MidiSystem es fundamental ya que interactúa con los recursos MIDI de nuestro sistema. Para obtener los 4 tipos de dispositivos básicos predeterminados del sistema usamos: MidiSsytem.getSequencer(); MidiSystem.getSynthesizer(); MidiSystem.getTransmitter(); MidiSystem.getReceiver(); Cada uno de estos métodos devuelve el objeto propio determinado por el sistema. Para usarlos debemos usar una variable de referencia del tipo correcto e igualarla al método que necesitamos. Supongamos que queremos el sintetizador por 168 defecto del sistema, que para toda aplicación Java siempre o casi siempre es 'Java Sound Synthesizer': Synthesizer sinte = MidiSystem.getSynthesizer(); Este código debe ir dentro de un try-catch ya que recordemos que puede no haber ningún sintetizador, secuenciador, transmisor o receptor disponible en el sistema porque otra aplicación puede estarlos usando. En las aplicaciones reales debemos manejar de forma correcta este error, no basta un anuncio en la ventana de salida porque recordemos que los usuarios no tienen una ventana de salida. Lo mejor es por lo menos mostrarle al usuario un informe del error que le advierta que no se ha podido crear un dispositivo porque el sistema lo tiene ocupado o porque la memoria es insuficiente. Una forma más robusta puede ser informarle del error y permitirle escoger otro dispositivo de la lista, si es que hay más disponibles. Otro punto a tener en cuenta es que una aplicación MIDI no tiene que poder manejar entradas y salidas. Si bien el MIDI fue inventado para la comunicación entre aparatos musicales, hoy día se usa en diferentes escenarios en los que esto no ocurre. Aunque sabemos que el MIDI es un protocolo de comunicación y que NO es sonido ni produce sonidos por sí mismo, Java si tiene una biblioteca de sonidos que podemos usar a través de MIDI, al igual que la gran mayoría de tarjetas de sonido de los computadores. Es por esto que una aplicación MIDI podría ser simplemente un metrónomo, de esta forma nos estamos aprovechando de la exactitud, el tiempo y en general de la información MIDI para crear una aplicación que por sí sola no necesita conectarse a nada más. También podríamos usar el MIDI en Java para hacer la música de juegos, así ésta podría ser manipulada de forma dinámica, esto quiere decir a medida que se desarrolla el juego. Además la música creada a partir de MIDI pesa muy poco. 169 La información MIDI Para poder entender a fondo y aprender realmente qué es MIDI, debemos conocer su lenguaje, su información, su estructura básica, esto nos obliga a entender cómo se organizan y qué dicen sus bytes de información. Esta información MIDI puede darse en dos escenarios principales: eventos en tiempo real o eventos guardados en un archivo o una memoria. Los primeros no necesitan una estampilla de tiempo ya que la idea es que la información se entregue y se haga algo con ella inmediatamente. En el segundo escenario, sí necesitamos estampillas de tiempo para saber en qué punto disparar cada uno de los eventos MIDI. Pueden coexistir ciertas aplicaciones que manejen simultáneamente ambos escenarios, por ejemplo en un concierto una agrupación musical puede tocar con pistas MIDI y al mismo tiempo generar sus propios eventos MIDI en tiempo real, por ejemplo para disparar otras secuencias. Una secuencia MIDI no es más que una sucesión de eventos MIDI guardados con estampillas de tiempo. La forma en que se organiza la información MIDI es muy simple. Un mensaje MIDI puede requerir uno o más bytes para dar el mensaje completo. Como ya dijimos antes, para hacer sonar una nota y luego callarla, necesitamos 6 bytes de información. Un mensaje MIDI completo traducido al español puede ser 'Toca el Do central relativamente fuerte', en MIDI para decir esto necesitamos tres bytes: el primero que dice enciende la nota es el byte 144, luego para decir que toque el Do central usamos el número 60 y por último una velocidad relativamente alta es 100. En conclusión para hacer sonar una nota desde MIDI necesitamos los siguientes tres valores: 170 Todo mensaje MIDI empieza con un status byte que en este caso es el número 144 que sirve para encender una nota, este status byte es llamado NOTE-ON. Reconocemos un status byte porque sus valores son entre 128 y 255. El número 144 significa encender una nota. Luego del status byte, para completar el mensaje siguen los data bytes. En este caso tenemos dos: 60 y 100. Reconocemos los data bytes porque tienen números entre 0 y 127. Los valores que dicen la nota y la dinámica del sonido son data bytes, por eso sus valores van dentro de este rango. Por ejemplo para los valores de las notas, tenemos 128 notas disponibles, siendo 0 la más grave y 127 la más aguda, donde 60 es el Do central. Para escoger qué tan duro suena un sonido, llamado velocidad o velocity por ser una representación de qué tan rápido se oprimen las teclas en un sintetizador, escogemos valores entre 0 y 127, donde 0 es silencio y 127 es lo más duro que puede sonar. Para callar dicha nota necesitamos otros tres bytes y hay dos formas de lograrlo. Podemos enviar la misma información pero con velocity igual a 0: 144, 60, 0 O también podemos usar el status byte para callar una nota o NOTE-OFF que es 128 con el mismo valor de data bytes usados al encender la nota: 128, 60, 100 Normalmente la mejor aproximación es usar el status byte NOTE-ON con velocity 0 para silenciar el sonido, ya que algunos dispositivos no adoptan NOTE-OFF. Los mensajes MIDI se transmiten entre canales. Por ejemplo, supongamos dos dispositivos conectados vía MIDI, la idea es controlar uno de los dos mediante el otro. El que controla lo denominamos master y el que se deja controlar vía MIDI lo llamamos esclavo. Normalmente los dispositivos se pueden configurar para enviar y recibir MIDI por un canal particular. Podemos pensar los canales como vías de 171 comunicación, si tanto el master como el esclavo están en el mismo canal, entonces podrán comunicarse, si por lo contrario se encuentran sintonizados en diferentes canales de comunicación, entonces aunque están enviando y recibiendo bytes físicamente, esta información está siendo ignorada. Cuatro de los bits en la mayoría de status byte, son utilizados para determinar el canal al que se está transmitiendo. Si cuatro bits se usan para determinar el canal, solo tenemos 16 diferentes posibles números, esto quiere decir que en MIDI sólo podemos tener 16 canales. Si bien en aplicaciones demasiado grandes y en sistemas muy avanzados podemos llegar a necesitar más de 16 canales, y esto es posible con ciertos dispositivos y ciertas técnicas, este tema se sale de los límites de este proyecto de grado, esto quiere decir que trabajaremos como si sólo tuviéramos a nuestra disposición 16 canales. Un dispositivo puede enviar información a varios canales, otro dispositivo puede poner cuidado a varios canales a la vez o solo a uno, esto depende de las características de cada uno. Antes dijimos que el status byte 144 era el NOTE-ON, esto es verdad, sólo que hay que aclarar que éste es el NOTE-ON para el canal 1. Todo dispositivo sintonizado en el canal 1 hará sonar esta nota, el resto no. Para hacer un NOTEON en el canal 2 usamos el 145, para el canal 3 usamos el 146 y así sucesivamente. En la siguiente tabla vemos los números para NOTE-ON y NOTEOFF para cada canal: CANAL NOTE-ON NOTE-OFF 1 144 128 2 145 129 3 146 130 4 147 131 5 148 132 6 149 133 7 150 134 8 151 135 172 9 152 136 10 153 137 11 154 138 12 155 139 13 156 140 14 157 141 15 158 142 16 159 143 Los canales son muy útiles para seleccionar un instrumento por canal. Si bien podemos cambiar el instrumento por canal en cualquier momento, normalmente cuando creamos una secuencia, cada instrumento va en un canal diferente. Por ejemplo, normalmente se usa que las baterías, percusiones e instrumentos rítmicos vayan en el canal 10. Además de los NOTE-ON, NOTE-OFF, notas, velocidad y canales, existe otra información MIDI que nos puede ser muy útil. El Pitch Bend Change es otro mensaje que deseamos enviar, sirve para modificar la altura de una nota. Este mensaje se envía usando los status byte del 224 al 239 para cada uno de los 16 canales respectivamente. Como el oído humano es capaz de sentir cambios muy pequeños de tono, 128 valores que nos permite un primer data byte no son suficientes. Por esta razón, el mensaje Pitch Bend Change necesita 2 data bytes para ser lo suficientemente preciso y así poder hacer cambios graduales que el oído no sienta como saltos sino como un cambio continuo. En el siguiente capítulo sobre bancos de sonidos hablaremos más a fondo sobre los mismos. Recordemos que el MIDI no son sonidos sino un protocolo que permite comunicar aparatos, pero en definitiva cada sintetizador o módulo debe tener sus propios sonidos para así poder hacer música desde la información MIDI que recibe. Estos sonidos son guardados en bancos de hasta 128 programas. 173 Para cambiar de programa usamos el mensaje Program Change que utiliza los status byte desde el 192 al 207 para los 16 canales respectivamente. Este mensaje necesita un solo data byte para declarar el número de programa al cual se quiere cambiar. Como ya dije antes, un banco puede tener hasta 128 programas, pero un mismo sintetizador puede tener muchos bancos de sonido. Para cambiar el banco usamos otro famoso mensaje MIDI llamado Control Change. Controles como la modulación, el pedal de sustain, el paneo, el volumen y otros efectos también son llamados o disparan el mensaje Control Change. Los status byte para este mensaje van desde el número 176 hasta el 191 para los 16 canales respectivamente. Este mensaje necesita dos data bytes, el primero determina el número de controlador, la siguiente lista dice los números de controladores correspondientes para cada parámetro: Controlador Número (data byte) Banco de sonidos 0 Rueda de modulación 1 Volumen 7 Paneo 10 Pedal de sustain 64 Si bien esta lista es bastante reducida, estos son los controladores más usados que especificamos en el primer data byte. En el segundo data byte escribimos el valor del controlador, por ejemplo el paneo usa el 0 para especificar lo más a la izquierda posible y 127 para una posición de panorama lo más a la derecha posible. Para especificar un Control Change de paneo en el canal 2 para un panorama totalmente a la izquierda usamos los siguientes tres bytes: 177, 10, 0 174 Con estos conocimientos podemos crear nuestra primera pequeña secuencia. Para lograrlo, necesitamos tres objetos básicos más los mensajes MIDI. El primer objeto es un secuenciador de tipo Sequencer, como vimos antes, usamos MidiSystem.getSequencer() para obtener un objeto de tipo Sequencer predeterminado por el sistema y así no preocuparnos por escoger uno de la lista. Este secuenciador se conectará automáticamente con un sintetizador así que no es necesario crear un Synthesizer. El segundo objeto que necesitamos es una secuencia de tipo Sequence que nos permite crear el tercer tipo de objeto que necesitamos que es un Track. Sequence sirve crear la estructura de datos MIDI para crear una canción, una secuencia se compone de varios Track que por lo general son los encargados de guardar la información de cada instrumento. Una secuencia puede tener una cantidad ilimitada de tracks, pero cada track puede tener hasta 16 canales. Además de los tres objetos Sequencer, Sequence y Track, necesitamos crear los eventos MIDI que vamos a agregar al Track. El siguiente código crea la estructura básica de una aplicación de este tipo y genera una única nota que dura un tiempo: import javax.sound.midi.*; public class Main { public static void main(String[] args) { try { // Crea y abre un secuenciador Sequencer secuenciador = MidiSystem.getSequencer(); secuenciador.open(); // Crea una secuencia con resolución de 4 ticks por negra Sequence secuencia = new Sequence(Sequence.PPQ, 4); // Crea un Track Track track = secuencia.createTrack(); // NOTE-ON en tick 1 ShortMessage mensaje1 = new ShortMessage(); 175 mensaje1.setMessage(144, 60, 100); MidiEvent evento1 = new MidiEvent(mensaje1, 1); track.add(evento1); // NOTE-OFF en tick 5 ShortMessage mensaje2 = new ShortMessage(); mensaje2.setMessage(144, 60, 0); MidiEvent evento2 = new MidiEvent(mensaje2, 5); track.add(evento2); // Agrega la secuencia al secuenciador secuenciador.setSequence(secuencia); // Empieza la secuencia secuenciador.start(); // No olvidar cerrar el secuenciador } catch (Exception e) { System.out.println(e); } } } Analiza el código para que veas cómo se crean los diferentes objetos. Como puedes ver, crear un mensaje y añadirlo al track requiere 4 líneas. Primero se crea un objeto de tipo ShortMessage al cual se le agrega un mensaje mediante el método setMessage() que recibe 3 parámetros, todos números enteros, el primero es el status byte, el segundo y el tercero son los data bytes: 176 Luego creamos un MidiEvent cuyo constructor nos pide el mensaje que creamos anteriormente y además nos pide el número de tick en el que queremos que se dispare dicho evento. Un tick no es más que la subdivisión mínima que hemos decidido entre pulsos por negra. Cuando creamos una nueva secuencia, el constructor nos pide dos valores, el primero puede ser Sequence.PPQ, esto significa que nuestro valor base de referencia son las figuras negras musicales. Sin embargo podríamos usar Sequence.SMPTE_24 o terminado en 25, 30 ó 30DROP que pueden llegar a ser muy útiles trabajando con medios visuales ya que significa que nuestro valor base ya no es la figura musical sino la cantidad de cuadros por segundo. Cada uno de estos cuadros, o cada una de las negras, se divide en ticks. La cantidad de éstos por unidad de división se determinan como segundo parámetro del constructor de Sequence. La resolución o cantidad de ticks por unidad de división dependen de qué tantos de ellos necesitemos. Por ejemplo si queremos usar semicorcheas, y sabemos que la ejecución musical no va a tener valores de figuras menores, entonces podemos crear una secuencia con valor de división de negras con 4 ticks por negra. Si sabemos que el valor mínimo son las fusas, entonces usamos 8 ticks por negra. Si queremos ser lo suficientemente amplios podemos usar valores más grandes. Volviendo al objeto MidiEvent, a su constructor le pasamos el mensaje y el tick en el cual queremos que se dispare el evento, en el ejemplo anterior escogimos 4 ticks por negra. Si queremos que un evento NOTE-ON se dispare en el primer pulso de una canción, entonces en MidiEvent seleccionamos el tick 1, si queremos que dure todo un compás 4/4, entonces disparamos el NOTE-OFF en el tick 17 para esta resolución. Para escoger la cantidad de pulsos musicales por minuto, esto es la velocidad de ejecución de la canción en BPM o Beats Per Minute o tempo, podemos usar el método setTempoInBPM() de Sequencer que recibe un float como parámetro. En el código anterior podríamos escoger un tempo de 82 BPM usando la siguiente línea antes de empezar la secuencia: 177 secuenciador.setTempoInBPM(82); La imagen anterior es una representación de Sequence(Sequence.PPQ, 4), esto quiere decir que la negra es la división base y tenemos 4 ticks por división. La duración de cada tick y de cada beat o pulso está determinada por la velocidad o tempo de la canción. Si cambiamos el tempo, cambia la duración de cada tick. Si por el contrario quisiéramos ticks que se mantuvieran en duración independientemente del tempo, podemos usar el siguiente ejemplo: En este caso estamos usando Sequence(Sequence.SMPTE_24, 2), que dice que nuestra división base son 24 cuadros por segundo y cada división tiene 2 ticks. Esto nos permite saber que independientemente del tempo, cada 12 cuadros o cada 23 ticks tenemos medio segundo. Es importante saber que el MIDI posiciona los eventos sobre un tick y no en medio de éstos, esto quiere decir que aproxima al más cercano y si la resolución no es la correcta podríamos oír notas fuera del tiempo. En resumen, necesitamos 6 pasos para crear mensajes MIDI que son leídos por un sintetizador escogido por defecto por Java. 178 1. Creamos y abrimos un secuenciador: Sequencer secuenciador = MidiSystem.getSequencer(); secuenciador.open(); 2. Creamos una secuencia con su respectiva resolución: Sequence secuencia = new Sequence(Sequence.PPQ, 4); 3. Creamos un track para la secuencia: Track track = secuencia.createTrack(); 4. Creamos un mensaje y lo agregamos al track: ShortMessage mensaje1 = new ShortMessage(); mensaje1.setMessage(144, 60, 100); MidiEvent evento1 = new MidiEvent(mensaje1, 1); track.add(evento1); 5. Agregamos la secuencia al secuenciador: secuenciador.setSequence(secuencia); 6. Empezamos a reproducir la secuencia: secuenciador.start(); Es importante saber que los archivos MIDI en su estructura básica usan la cantidad de ticks desde el último evento MIDI para almacenar el punto exacto de disparo del evento. Esto quiere decir que si el primer evento MIDI ocurre en el tick 179 10 y el segundo evento MIDI se va a disparar en el tick 30, los archivos MIDI dicen que el segundo evento debe dispararse en el tick 20 ya que esta es la diferencia entre el último evento MIDI. Sin embargo, en Java no es necesario pensar en estas diferencias entre ticks para disparar eventos. En Java debemos escribir la cantidad de ticks totales para decir cuándo debe ocurrir un evento. Seguramente te habrás dado cuenta que para poder crear secuencias complejas, vamos a tener que repetir muchas veces las cuatro líneas que crean un mensaje MIDI. Si vamos a hacer sonar 10 notas, necesitamos 20 mensajes, 10 para NOTEON y 10 para NOTE-OFF. Si cada mensaje necesita 4 líneas de código, esto quiere decir que para hacer sonar 10 notas necesitamos 80 líneas de código. Esto no es nada práctico y la verdad es que muchas veces necesitamos crear cientos de notas en una misma aplicación. ¿Cómo podemos optimizar el proceso? Utilizando una clase creada por nosotros que nos permita crear una sola línea de código para enviar un evento MIDI. La mejor opción es crear una clase que nos funcione para varias utilidades MIDI. El nombre de la clase va a ser UtilidadesMidi. En este clase podríamos crear muchos métodos que nos pueden ahorrar trabajo en futuros proyectos al crear aplicaciones Java. Considera el método estático Notas() en la clase UtilidadesMidi como ejemplo para crear tus propios métodos. Este método es estático ya que no pretendo poner al usuario a crear un objeto de esta clase para este método, la idea es poder devolver el MidiEvent necesario para crear un mensaje MIDI sin tener que escribir las cuatro líneas antes necesarias. El método Notas() recibirá el status byte, dos data bytes, y el número de pulso en que vamos a crear el evento. Para este ejemplo estoy limitando a negras la posibilidad de disparo de eventos, obviamente podríamos modificar y mejorar este código, lo importante es entender la importancia de la programación orientada a objetos. Este método no es muy cercano a los objetos por ser estático, pero con este ejemplo puedes ver que el hecho de tener una clase que podríamos mejorar, llenar de otros métodos y convertir en un objeto, nos mejora y optimiza nuestro código: 180 import javax.sound.midi.*; public class Main { public static void main(String[] args) { try { Sequencer secuenciador = MidiSystem.getSequencer(); secuenciador.open(); Sequence secuencia = new Sequence(Sequence.PPQ, 24); Track track = secuencia.createTrack(); track.add(UtilidadesMidi.Notas(144, 57, 100, 1)); track.add(UtilidadesMidi.Notas(144, 57, 0, 2)); track.add(UtilidadesMidi.Notas(144, 61, 100, 2)); track.add(UtilidadesMidi.Notas(144, 61, 0, 3)); track.add(UtilidadesMidi.Notas(144, 64, 100, 3)); track.add(UtilidadesMidi.Notas(144, 64, 0, 4)); track.add(UtilidadesMidi.Notas(144, 71, 100, 4)); track.add(UtilidadesMidi.Notas(144, 71, 0, 5)); track.add(UtilidadesMidi.Notas(144, 69, 100, 5)); track.add(UtilidadesMidi.Notas(144, 69, 0, 9)); secuenciador.setSequence(secuencia); secuenciador.setTempoInBPM(90); secuenciador.start(); while (secuenciador.isRunning()) { if (secuenciador.getTickPosition() >= ((9 * 24) - 23)) { secuenciador.close(); } } } catch (Exception e) { System.out.println(e); } 181 } } class UtilidadesMidi { public static MidiEvent Notas(int status, int data1, int data2, int quarter) { ShortMessage mensaje = null; try { mensaje = new ShortMessage(); mensaje.setMessage(status, data1, data2); } catch (Exception e) { System.out.println(e); } return new MidiEvent(mensaje, ((quarter * 24) - 23)); } } Si compilas y ejecutas el código anterior, oirás el sonido promocional de www.ladomicilio.com. Al terminar de sonar, la aplicación se cerrará, para lograr esto cerramos el secuenciador comprobando constantemente en un ciclo el momento en el que la secuencia alcanza el pulso número 9. Para crear esta pequeña secuencia usamos únicamente 10 líneas encargadas de los eventos MIDI, sin la el método Notas() hubiéramos necesitado 40 líneas de código. Si bien este código se puede mejorar de muchísimas formas y es apenas una simple base demostrativa, quiero que quede como inquietud para que explores y te des cuenta con todo lo aprendido en la sección de Objetos de este proyecto de grado, que siempre que veas un código que se va a repetir mucho, es buena idea que crees tus propias clases. Esto no sólo te ayudará en la aplicación que estés trabajando en el momento, ya que si lo haces lo suficientemente genérico, podrás reutilizarlo en muchas otras aplicaciones y así no tendrás que reinventar la rueda. 182 Bancos de sonido En este punto ya somos capaces de seleccionar los recursos MIDI del sistema y podemos permitir comunicación entre ellos. Además hemos entendido de forma precisa y particular cómo funciona el lenguaje para crear nuestros primeros mensajes. Para poder hacer secuencias reales que usen más de un instrumento y para poder cambiar un sonido de un aparato usando MIDI, debemos entender a fondo los canales, los programas, los bancos e incluso debemos aprender cómo generar nuestros propios sonidos. "Instruments are organized hierarchically in a synthesizer, by bank number and program number. Banks and programs can be thought of as rows and columns in a two-dimensional table of instruments. A bank is a collection of programs. The MIDI specification allows up to 128 programs in a bank, and up to 128 banks. However, a particular synthesizer might support only one bank, or a few banks, and might support fewer than 128 programs per bank. "(The Java Tutorials, 2010: Synthesizing Sound) Es clave entender la estructura de sonidos. Los bancos guardan varios programas y los programas son la representación a un sonido particular, ya sea grabado o producido mediante un computador. Un aparato puede tener hasta 128 bancos, cada uno con hasta 128 programas, sin embargo pueden usar muchos menos bancos y muchos menos programas por banco. No debemos olvidar que el MIDI no tiene sonidos por si solo ya que éste es sólo un protocolo de comunicación. Son los sintetizadores, módulos de sonido, samplers8 y software los que de por si tienen sonidos que son disparados vía MIDI. Dentro del mundo MIDI, existen especificaciones llamadas prácticas recomendadas. Una de éstas es el General MIDI que no es más que una forma de 8 Un sampler es un aparato capaz de grabar un sonido para luego ser usado vía MIDI. Por lo general los samplers vienen con las herramientas necesarias para editar dicho sonido de tal forma que con una sola muestra, se puedan recrear varias alturas del mismo. 183 ordenar el timbre de los sonidos en un número de programa específico, además de otros puntos para que la información MIDI sea consistente de un aparato a otro. Un punto de General MIDI dice que el Do central siempre será el número 60 en un data-byte. Imaginemos el problema que podría causar un aparato que no siguiera esta recomendación. Otro punto que dice General MIDI es que el canal 10 es exclusivo para instrumentos de percusión. Si usamos el último programa que escribimos en el capítulo pasado, pero en vez de generar NOTE-ON en el canal 1 los generamos en el canal 10, podremos oír que nuestra secuencia se cambia a timbres de percusión. Para esto lo único que debemos hacer es cambiar el número 144 que es el NOTE-ON para el canal 1, por el número 153 que es el NOTE-ON para el canal 10. Recordemos que también podemos enviar un evento MIDI muy usado llamado Program Change que es el encargado de cambiarnos el timbre del instrumento. Program Change para el canal 1 es el número 192, este status byte necesita un único data-byte que especifica el programa al que queremos cambiar. Por ejemplo, una guitarra acústica es el número 24, con el siguiente código podemos hacer el cambio: mensaje.setMessage(192, 24, 0); Como no necesitamos un tercer byte, podemos simplemente escribir cero. Podemos saber que el número 24 es una guitarra acústica porque así lo determina el General MIDI, sin embargo, si estamos en otro banco o si el aparato no soporta General MIDI, entonces obtendremos otro sonido. Afortunadamente el sintetizador de Java sigue las recomendaciones de General MIDI. Con esto queda claro que cada canal va a ser el encargado de un único instrumento a la vez, un mismo canal puede cambiar cuantas veces quiera de programa, pero no puede tener dos programas al mismo tiempo. Esto quiere decir que estamos limitados por los 16 canales que nos brinda MIDI. Para hacer sonar más de un instrumento a la vez simplemente usamos los canales. En el canal 1 podemos tener un bajo, en el 2 un 184 piano, en el 3 una guitarra, en el 10 la percusión, etc. Para saber qué instrumentos van en qué número de programa según General MIDI, podemos referirnos a la siguiente lista: (Rona, 1994: 68) En esta imagen del libro 'The MIDI companion', vemos la lista ordenada de los instrumentos como recomienda General MIDI. Debemos ser cuidadosos porque está ordenada del 1 al 128, pero recordemos que los números MIDI para databytes van desde el 0 hasta el 127, así que si queremos escoger por ejemplo el violín que vemos en la tabla en el número 41, en Java debemos usar el número 40. Si estamos usando el canal 10 para instrumentos de percusión, debemos 185 saber que nota corresponde a cuál timbre. Para eso podemos referirnos a la siguiente tabla: (Rona, 1994: 69) Cuando tengamos un sintetizador que no se encuentre en el banco correcto, podemos modificar su número de banco mediante un Control Change. Recordemos que los Control Change van desde el número 176 hasta el 191, correspondientes a los 16 canales. El número de controlador que modifica el banco es el data-byte número 0. El tercer byte es el número de banco al que queremos cambiar. El siguiente código cambia al banco 10 de un sintetizador en el canal 2: mensaje.setMessage(177, 0, 10); Más adelante, en la parte de audio, aprenderemos a generar sonidos a partir de ecuaciones creadas en Java, por ejemplo una onda seno. Imaginemos que queremos disparar nuestros propios sonidos desde un controlador externo como el M-AUDIO que tengo conectado al computador. Para lograrlo podemos proceder de varias formas pero todas involucran implementar una de las clases del API de Java. Podríamos crear nuestro propio sintetizador creando nuestra propia clase 186 que implemente Synthesizer. Sin embargo, debido a la extensión que esto implicaría, vamos a hacer un ejemplo muy parecido implementando la interfaz Receiver. Como todavía no sabemos cómo crear nuestros propios sonidos partiendo de Java, ni tampoco sabemos cómo reproducir sonidos guardados en el computador, vamos a crear una aplicación que muestre la tecla presionada en el controlador en la ventana de salida. Más adelante puedes usar este mismo código y reemplazar la impresión en la ventana de salida por la ejecución de un sonido. import javax.sound.midi.*; class Main { public static void main(String[] args) { MidiDevice.Info[] dispositivos = MidiSystem.getMidiDeviceInfo(); try { MidiDevice aparato = MidiSystem.getMidiDevice(dispositivos[2]); aparato.open(); Transmitter maudio = aparato.getTransmitter(); Receiver receptor = new Receptor(); maudio.setReceiver(receptor); } catch (Exception e) { System.out.println(e); } } } class Receptor implements Receiver { public void close() { // se puede dejar vacío. } public void send(MidiMessage mensaje, long tiempo) { for(byte i = 0; i < mensaje.getLength(); i++) { System.out.print((int) (mensaje.getMessage()[i] & 0xFF) + ", "); 187 } System.out.println(); } } Esta aplicación muestra en la ventana de salida el mensaje MIDI que obtiene el programa cuando presiono alguna tecla, la suelto, uso el Pitch Bend, etc. El principio básico de lo que ocurre en main() es muy parecido al código que vimos en el capítulo sobre comunicación MIDI cuando usamos el controlador para producir sonido en el sintetizador. La diferencia es que en este caso el receptor es una instancia de nuestra clase que implementa Receiver. Esta clase la hemos llamado Receptor y por implementar Receiver está obligada a sobrescribir los métodos close() y send(). En este caso el método que nos interesa es send() que recibe como parámetros el mensaje MIDI y el momento en el tiempo de la ejecución. El mensaje MIDI es una instancia de MidiMessage, esto permite llamar sobre ésta el métodos getLength() que devuelve el largo en bytes del mensaje recibido por el controlador, y el método getMessage() que devuelve un arreglo de bytes con el status-byte y los data-bytes del mensaje. Debido a cierto comportamiento de Java que no voy a detenerme a explicar en este punto, es necesario hacer una conversión usando & 0xFF y un cast (int) para obtener el número de bytes correcto. Sin este código obtendríamos números que no son iguales a los que venimos manejando, si quieres aprender más al respecto puedes buscar sobre el operador bitwise. El ejemplo anterior demuestra dos puntos muy importantes. Primero, podemos implementar las interfaces o incluso podemos extender clases del API de MIDI de Java para poder hacer virtualmente lo que queramos. Segundo, al implementar Receiver, podemos saber exactamente el mensaje que envía un controlador o un aparato externo, esto sumado a todo lo que permite Java, nos da la posibilidad de crear casi cualquier aplicación que imaginemos. Por ejemplo podríamos usar el teclado para mostrar diferentes dibujos en pantalla. 188 Archivos MIDI Cuando es hora de guardar nuestras secuencias, usamos los archivos MIDI para almacenarlas. Como ya hemos visto, la clase MidiSystem nos provee varios métodos muy útiles para trabajar con MIDI, entre ellos encontramos uno llamado write() encargado de guardar de forma muy fácil este tipo de archivos. Antes de intentar usar este método hay un par de conocimientos que debemos adquirir. Los archivos MIDI se guardan en extensiones '.mid'. Existen 2 tipos de archivos MIDI soportados de forma general por los computadores y aparatos musicales: tipo 0 y tipo 1. El tipo 0 es para archivos MIDI con un solo Track. Hasta ahora hemos creado ejemplos con un único track así que nuestras secuencias podrían guardarse en este tipo de archivos. El tipo 1 se usa para secuencias que usen más de un Track y en la gran mayoría de secuencias reales con varios instrumentos, la mejor opción es crear un nuevo Track para cada uno de los instrumentistas de la secuencia, probablemente cada uno con su propio canal. Los archivos MIDI usan un tipo de datos llamados meta-eventos. En éstos se almacenan datos como el tempo, nombre del track, texto de la canción y otros. No pretendo entrar en detalles sobre cómo es la estructura de un archivo MIDI, con que sepas que existen los meta-eventos y cómo se crea uno de ellos, es más que suficiente para que investigues el resto. Antes habíamos dicho que con el método setTempoInBPM() de Sequencer, podíamos escoger el tempo de una canción. Este método tiene una falencia y es que es muy útil cuando NO vamos a crear un archivo MIDI de la secuencia, pero el problema es cuando queremos guardarla, ya que la información del tempo es almacenada como un meta-evento, así que el método setTempoInBPM() es totalmente ignorado al guardar usando el método write() de MidiSystem. Cada vez que hemos escrito y añadido un evento MIDI a un track, hemos usado el objeto ShortMessage, sin embargo, para crear metaeventos, necesitamos usar el objeto llamado MetaMessage. El siguiente código, 189 que voy a escribir fuera de su contexto, es el encargado de crear el meta-evento que permite añadir el tempo a una secuencia. int tempo = 60000000/60; byte[] data = new byte[3]; data[0] = (byte)((tempo >> 16) & 0xFF); data[1] = (byte)((tempo >> 8) & 0xFF); data[2] = (byte)(tempo & 0xFF); MetaMessage meta = new MetaMessage(); meta.setMessage(81, data, data.length); MidiEvent evento = new MidiEvent(meta, 0); track.add(evento); El tempo en una secuencia se escribe en microsegundos por pulso. Esto quiere decir que si el tempo deseado es 60bpm, el número que usaremos en el metaevento es 60000000 dividido 60. Siempre debemos dividir 60000000 entre el pulso en pulsos por minuto. El objeto MetaMessage usa el método setMessage() para crear el mensaje, este método necesita tres argumentos. El primero es el número del meta-evento, que siempre es un número menor a 128, para el caso del tempo es el número 81. De segundo recibe un arreglo del tipo byte con la información necesaria para el evento. En este caso son tres bytes que uno al lado del otro dicen el número del tempo en microsegundos por pulso, pero como debemos pasar dicho número en tres bytes, debemos usar ciertas matemáticas que se salen de los límites de este proyecto de grado ya que no necesitas llegar a manejar este tipo de conocimientos para crear tus primeras aplicaciones. Lo importante es que entiendas que allí estamos tratando de convertir el tempo que necesitamos, en su representación en tres bytes metidos en un arreglo. Si en algún momento decides usar otro tempo, simplemente debes cambiar el denominador de la división de la variable tempo, todo el contenido del arreglo data debes dejarlo tal y como aquí aparece. Si decides aprender por tu cuenta sobre lo que está ocurriendo en este código, te recomiendo que busques sobre 190 hexadecimales y operadores de bits en Java. Como tercer parámetro, le pasamos al método el largo del arreglo. El resto es exactamente igual a como hemos tratado los mensajes MIDI para agregarlos al track. Con esta información ya podemos guardar nuestra primera secuencia MIDI. No voy a crearla aquí ya que en este punto tienes todos los conocimientos para crear una. Después de crear tu primera secuencia, que supongamos ha quedado dentro de la variable secuencia, que es la variable de referencia al objeto Sequence, puedes usar el siguiente código dentro de un try-catch. MidiSystem.write(secuencia, 1, new FileOutputStream("secuencia.mid")); El método write() nos pide tres argumentos. El primero es la referencia a la secuencia. El segundo es el tipo de archivo MIDI, que como ya he dicho antes, puede ser 0 ó 1, incluso existe el tipo 2 pero no es ampliamente soportado. El tercer argumento es un objeto del tipo FileOutPutStream que podemos crear allí mismo, a su constructor le pasamos un String con la ubicación, el nombre y la extensión relativa de la ubicación en la que va a terminar el archivo guardado. Debemos tener en cuenta que FileOutPutStream es un objeto dentro del paquete io, por eso debemos importarlo para poder usarlo: import java.io.*; Por último, cuando queramos leer un archivo MIDI, por ejemplo el mismo que hemos creado anteriormente, simplemente usamos el método getSequence() de MidiSystem que recibe un objeto de tipo File que es creado allí mismo y también está en el paquete io. Al constructor de File le pasamos la dirección relativa del archivo y guardamos la secuencia en una variable de referencia de tipo Sequence: Sequence secuencia = MidiSystem.getSequence(new File("secuencia.mid")); 191 Edición de secuencias En muchas ocasiones queremos que nuestras aplicaciones tengan la capacidad de seleccionar una secuencia, modificarla y luego guardar dichos cambios. En este momento sabemos cómo crear una secuencia y también sabemos guardarla, pero no sabemos cómo hacer cambios sobre un evento MIDI anteriormente agregado. Para el proceso de edición hay muchos métodos que nos provee el API de MIDI de Java, sin embargo, quiero enfocarme en las posibilidades que nos brinda Track. Para editar un mensaje MIDI, primero debemos saber cómo seleccionarlo para saber cuál es su contenido. Todos los mensajes MIDI que se encuentran almacenados dentro de un track, se ordenan en fila uno detrás de otro, recordemos que la comunicación MIDI se da de forma serial. Por eso cuando estamos buscando un evento MIDI dentro de un track, debemos indicar su posición, tal y como hacemos cuando indicamos el índice de un arreglo. El método get() de la clase Track nos permite seleccionar los eventos MIDI dentro de ese Track, mediante un número que le pasamos como argumento que indica su posición, donde 0 es el primer mensaje, 1 es el segundo mensaje, 2 es el tercero, etc. El método get() devuelve un objeto de tipo MidiEvent que ya hemos visto antes, sobre éste podemos usar el método getMessage() que devuelve un objeto MidiMessage sobre el cual podemos usar nuevamente getMessage() que devuelve un arreglo de bytes con los números de los bytes del mensaje. El siguiente código está fuera de contexto y por si solo no compila, si lo usas sobre una secuencia que tenga una variable de referencia llamada track que haga referencia a un Track con mensajes MIDI, verás en la ventana de salida el mensaje que devuelva get() según el número de índice: MidiEvent primerEvento = track.get(4); MidiMessage primerMensaje = primerEvento.getMessage(); 192 byte[] numeros = primerMensaje.getMessage(); for(byte i = 0; i < numeros.length; i++) { System.out.print((int) (numeros[i] & 0xFF) + ", "); } En el ejemplo anterior estamos pidiendo el evento número 5 de track. Uno de los resultados posibles puede ser: 144, 57, 0, Este resultado expresa un NOTE-ON sobre la nota 57 que es un La, con un velocity de cero, por lo tanto su función es apagar esta misma nota antes encendida. Dentro del ciclo estamos pasando por cada uno de los byte del arreglo numeros. A cada elemento le hacemos la conversión necesaria (int)(numeros[i] & 0xFF) para convertir el valor de cada uno de los elementos del arreglo numeros a un valor que nosotros podemos reconocer como status y data-bytes. Dentro del print() hemos agregado un String que contiene una coma para poder separar visualmente cada uno de los bytes del mensaje. Podemos usar el método size() de Track que devuelve la cantidad de mensajes MIDI almacenados por dicho track. Ahora que hemos obtenido el mensaje, podríamos hacer varias cosas con él. Si quisiéramos podríamos borrar este mensaje mediante el método remove() de Track, el cual recibe como argumento un objeto de tipo MidiEvent. Para borrar el mensaje 18 de un Track, usamos el siguiente código: boolean borrando = track.remove(track.get(17)); Recordemos que escribimos 17 dentro de get() porque los índices empiezan desde cero, entonces el mensaje 18 es en realidad el índice 17. El método 193 remove() devuelve un booleano true si logró borrar con éxito el evento y false si no lo logró. Una vez obtenemos un mensaje, varios o todos los mensajes MIDI, podríamos hacer una aplicación que mostrara una partitura con dicha información. Esto obviamente requiere un trabajo extenso con GUI y MIDI, pero es totalmente posible en Java. Con los mensajes que obtenemos, también podríamos borrar el mensaje original y crear uno nuevo partiendo de la información dada. Esto con el fin de corregir errores en nuestra secuencia o hacer modificaciones sobre la misma. Por ejemplo si queremos modificar la duración de una nota, buscamos el NOTE-ON con velocity cero que la apaga, y luego usamos los mismos valores obtenidos, para crear un nuevo MidiEvent con el tick correcto. En este punto borraríamos el mensaje obtenido en un inicio y agregamos al track el nuevo MidiEvent modificado. Si bien el API de MIDI en Java es bastante completo y nos permite trabajar directamente con los bytes, normalmente su implementación requiere de varias líneas de código y en muchas ocasiones no es tan práctico. Mi consejo es que uses todos los conocimientos sobre programación orientada a objetos y crees nuevas clases que te permitan trabajar a futuro más fácilmente con el código MIDI. Es importante entender que hasta aquí he tocado puntos fundamentales que te permitirán crear tus primeras aplicaciones MIDI. Sin embargo, no he descrito todo el API de MIDI en Java. Hay varias interfaces y clases que se han quedado por fuera ya que hacen parte de procedimientos más complejos y aunque pueden ser muy útiles, son menos usados que los vistos hasta aquí. Te recomiendo que vayas y explores el API de MIDI en Java y descubras otros métodos útiles en las clases e interfaces vistas también. Sólo mediante la programación de tus propias aplicaciones podrás aprender y entender realmente este gran tema. 194 Teoría de audio digital En este capítulo no pretendo hacer una descripción detallada de todos los conceptos de audio digital ya que lograrlo me tomaría demasiadas páginas y no es necesario profundizar en estos conocimientos para hacer nuestras primeras aplicaciones de audio en Java. Sin embargo, entre más conocimientos tengas sobre teoría de audio digital, mejores y más robustas aplicaciones de audio podrás crear. En este capítulo pretendo hacer un repaso y resumen de las bases que gobiernan este mundo para poder empezar con nuestras primeras aplicaciones sencillas que nos permitirán entrar en el mundo de la programación en Java enfocada al audio. Los mismos principios de bits y bytes que aprendimos en el primer capítulo de MIDI, son principios que también están presentes en el audio digital. Recordemos que los computadores manejan la información en unos y ceros, esto quiere decir en bits. La principal diferencia con el MIDI, es que 8 bits eran suficientes para dar un mensaje como NOTE-ON, y en general cada pequeña pieza de 8 bits era suficiente. En el audio, 256 valores posibles que podemos tener en un byte no son suficientes. El audio digital pretende hacer una representación lo más parecida a la realidad de la teoría de las ondas que viajan a través de los medios y que por estar entre 20Hz y 20.000Hz y ser captadas por el oído, hemos denominado ondas sonoras. Para poder entender qué están tratando de simular los bits de las ondas, primero debemos entender cómo son y cómo funcionan las ondas. Una onda no es más que la perturbación en tiempo y espacio de un medio elástico que permite transferir la energía que lo causa. Existen muchos tipos de ondas diferentes y es gracias a esto que oímos timbres, alturas y duraciones distintas. La representación más fácil de entender de una onda, es la onda seno. Esta onda es la representación de la función seno de matemáticas. Es fundamental que entendamos los elementos de las ondas para facilitar su futura comparación y 195 representación en el mundo digital. Las características básicas de una onda, las podemos ver en la siguiente gráfica: En la imagen vemos tres ondas sinusoidales, sobre la de la mitad estoy enumerando las características básicas de toda onda. 1. Amplitud: Es la variación máxima entre el punto de reposo o cero que es la línea recta horizontal que atraviesa las tres ondas, y el punto más alto de la onda. Existen otro tipo de medidas como la amplitud pico a pico que es la diferencia entre el punto máximo de la onda y el punto más bajo de la misma. Sin embargo cuando hablemos de amplitud, nos referiremos a la diferencia entre 0 y el punto más alto de la onda y en otras ocasiones nos sirve como referencia al valor que tiene una altura determinada de la onda desde cero así no sea la máxima. 2. Longitud de onda y ciclo: La onda en rojo en la imagen muestra un ciclo completo de onda que está dada entre los puntos dónde no ha comenzado a repetirse la misma. El ciclo está dado por la longitud de onda que es una medida de espacio entre el punto de comienzo y punto final de un ciclo en línea recta. 3. Valle: Es el punto más bajo que alcanza la onda. Por lo general se representa con números negativos ya que se toma la línea recta horizontal como el punto de equilibrio que es igual a cero. 196 4. Cresta: Es el punto más alto que alcanza la onda. Por lo general se da en números positivos por estar encima del punto de equilibrio o cero. La gráfica anterior pretende demostrar cómo se comporta en el tiempo una onda. La idea es pensarlo como un plano cartesiano donde hay un eje horizontal y uno vertical. Para la música usamos el eje horizontal para describir el paso del tiempo. En el vertical describimos la amplitud o perturbación del medio producido por la onda. Sin embargo, desde la porción matemática de crear una onda seno, normalmente usamos el eje horizontal para escribir números enteros que representan grados ya que el comportamiento de una onda seno está estrechamente relacionada con los mismos. Podríamos usar la siguiente ecuación para obtener la siguiente gráfica: y = sen x; En la anterior ecuación, y nunca será mayor a uno ni menor a menos uno. Los números en x pueden seguir para siempre y el resultado siempre será la repetición de la onda, donde un ciclo tiene una longitud de 360. Para poder relacionar esta onda creada desde las matemáticas con los sonidos que percibe nuestro oído, primero debemos dejar claro que el sonido se produce al perturbar un medio elástico como el aire, moviendo así las partículas desde el 197 punto de creación hasta nuestro oído, de una forma que podemos representar mediante la teoría de ondas que estamos aprendiendo, pero el sonido en sí es una percepción de nuestro oído como resultado a dicha explicación física. También debemos entender dos conceptos claves para poder comparar la onda seno con un sonido de la vida real, éstos son el período y la frecuencia. El período es el tiempo que transcurre mientras se da un ciclo completo de onda. La velocidad con la que se de este ciclo, determina la altura del sonido. Si un ciclo de onda se da más rápido, más agudo será el sonido, si el ciclo de la onda demora en completarse más tiempo, la onda se percibirá como más grave. La frecuencia es una medida en Hertz 'Hz' y determina la cantidad de ciclos que ocurren en un segundo, a mayor frecuencia obtendremos un menor período. Para hacer conversiones entre frecuencia y período, podemos usar las siguientes fórmulas, dónde f es frecuencia y T es período: T = 1/f f = 1/T El oído humano oye frecuencias entre 20Hz y 20KHz, aunque hay que tener presente que este rango es un estimado que con la edad va disminuyendo o incluso puede nacerse con un oído que percibe un rango más limitado y esto no necesariamente significa una anormalidad. Si lo queremos, podemos pasar la onda seno vista anteriormente al plano del audio digital. Para esto debemos pensar que necesitamos bits para representar por un lado la amplitud de la onda y otros bits para representar el tiempo en que ocurre una amplitud determinada. La primera pregunta a la que nos enfrentamos es cuántos valores necesitamos para representar una línea como la que describe una onda seno, cuántos valores en x y cuántos en y son suficientes para recrear esta onda a la perfección. Cuando el hombre decidió que quería hacer posible la filmación, se dio cuenta que no era posible capturar el movimiento de las cosas, lo 198 único que pudo hacer fue capturar muchos momentos precisos en fotos, pero el movimiento no pudo ni ha podido ser realmente capturado. Lo único que pudo hacer el humano fue tomar fotos tan rápido que al pasarlas a velocidades altas simulaba el movimiento. Exactamente a lo mismo nos enfrentamos en el mundo digital. No podemos capturar el movimiento exacto de la partículas en el aire debido a que el tiempo hacia lo mínimo es infinito, tampoco podemos capturar lo infinito que es internamente un ciclo de onda seno, lo que sí podemos hacer es tomar tantas muestras de momentos exactos de la onda seno que al final el resultado será una simulación que trata de aproximarse lo más posible a lo que ocurre en el mundo real. Supongamos que queremos representar de forma digital las siguientes ondas seno que ocurren en un segundo exacto, esto quiere decir que son ondas cuya frecuencia es 3Hz que no es audible pero para el ejemplo es totalmente útil: Como ya sabemos, necesitamos valores para representar el tiempo y valores para representar la amplitud en un momento dado, por lo tanto debemos decidir cuántas muestras vamos a tomar por segundo y cuántos bits para representar la amplitud. Supongamos que vamos a usar 2 bits para la amplitud y vamos a tomar en un segundo 5 muestras, esto quiere decir 4 valores posibles para la amplitud y una muestra cada 0,2 segundos. La siguiente imagen muestra los puntos en rojo de la amplitud para las 5 muestras tomadas en un segundo: 199 Hasta ahora hemos visto que una serie de bits pueden representar números de cero en adelante. Por ejemplo en MIDI vimos como 8 bits nos servían para representar números del 0 al 255, sin embargo podrían también representar los números del -128 al 127 que en total también son 256 valores. Para este caso pensemos que los dos bits que vamos a usar, representan los números del -2 al 1. Para los tres ciclos de la onda seno en un segundo, bajo nuestra resolución, el programa obtiene la siguiente tabla: Tiempo (segundos) Amplitud (2 bits) 0 0 0,2 -1 0,4 1 0,6 -2 0,8 1 1 -1 Un programa de audio digital que toma muestras, para luego reproducirlas, une los puntos trazando líneas, que dependiendo del formato y la codificación pueden no ser necesariamente líneas rectas sino líneas curvas o una combinación. Supongamos que nuestro sistema traza líneas rectas, la siguiente imagen muestra la comparación de ambas ondas, la original y el resultado bajo nuestra resolución en rojo: 200 Como podemos ver, el resultado bajo nuestra resolución demuestra un claro error al recrear las ondas originales. Si aumentamos sólo la cantidad de bits para recrear la amplitud de forma más precisa, igual tendríamos una recreación muy poco precisa debido a la poca cantidad de muestras. De la misma forma, si aumentamos la cantidad de muestras así sean 100.000 por segundo, con sólo 2 bits no es suficiente para representar las ondas de forma correcta. Afortunadamente en cuanto a la cantidad de bits que toman la amplitud no tenemos que preocuparnos ya que existen 2 cantidades de bits altamente usadas en el mundo del audio que son 16 bits y 24 bits. Con 16 bits podemos representar hasta 65536 diferentes puntos de amplitud. Con 24 bits existen 16.777.216 valores posibles. Aunque en Java se puede usar 24 bits, el API de sonido tiene algunas limitaciones hasta 16 bits, que de una u otra forma se pueden solucionar pero son temas avanzados para este proyecto de grado. Por ahora podemos mantenernos usando formatos con 16 bits y por tu cuenta puedes buscar cómo solucionar problemas cuando estés trabajando archivos de audio con 24 bits. En cuanto a la cantidad de muestras por segundo o frecuencia de muestreo, tenemos el teorema de muestreo de Nyquist-Shannon, que nos enseña cuál es la frecuencia de muestreo mínima que debemos usar para poder obtener una buena muestra de una onda. El teorema dice que la frecuencia de muestreo mínima debe ser igual al doble de la frecuencia máxima a muestrear. En este ejemplo estamos usando una frecuencia de 3Hz entonces el mínimo necesario son 6 muestras por segundo. Si aplicamos a nuestro ejemplo anterior 6 muestras por segundo que nos indica el 201 teorema y una cantidad de bits ilimitada para ser muy precisos, obtendremos el siguiente resultado: Al tratar de unir los puntos obtendremos una línea recta, esto quiere decir que bajo estas circunstancias, el resultado será cero, la onda no habrá sido muestreada. Aunque recordemos que un sistema bien podría no unir los puntos con líneas rectas, es claro que hay una deficiencia en las muestras ya que la onda podría ser cuadrada. Esto para nada quiere decir que el teorema de Nyquist-Shannon esté mal planteado. Si por ejemplo corremos la onda 90 grados, o si empezamos a tomar las muestras 1/12 de segundo antes o después, obtendremos en la muestra la cresta y el valle de cada ciclo, y aunque al unir esos puntos el resultado no sea exactamente una onda seno, si va a ser una onda con la misma frecuencia. Sin importar el punto donde empiecen las muestras, siempre que sea diferente al punto en que la onda está en cero, el resultado va a ser una onda con la misma frecuencia que la original. Si coincide con cero, la onda no se representará. Este ejemplo demuestra que el teorema no pretende determinar la frecuencia de muestreo mínima para obtener un resultado perfecto, simplemente es una forma de obtener un valor mínimo para evitar ciertos problemas como el aliasing que veremos a continuación. Debemos pensar el teorema de Nyquist-Shannon como una forma básica de determinar un mínimo de frecuencia de muestreo que no produzca errores audibles, y aunque este teorema se aleje de la calidad de la muestra, debemos recordar que el teorema no está pensado en cuanto a calidad sino en cuanto a solución de errores que se presentan cuando no cumplimos este requerimiento. 202 Existe un efecto conocido como aliasing que es causado por no cumplir el teorema de Nyquist-Shannon. Pensemos que nuestra onda de 3Hz va a ser muestreada 4 veces por segundo, esto incumple el teorema que dice que deben ser mínimo 6 muestras por segundo para una onda de 3Hz: Como puedes ver, la representación en rojo está recreando una onda totalmente diferente a las ondas originales, incluso parece crearse una especie de ciclo completo de una onda de 1Hz. La imagen demuestra el efecto aliasing que es cuando aparece la representación de una onda que no existía en un comienzo y fue creada por no seguir el teorema de Nyquist-Shannon. En un ejemplo de la vida real, algunas veces queremos crear aplicaciones de transmisión de voz, en las que la calidad e integridad de todo el rango audible no es tan importante, sino transmitir un mensaje claro, inteligible y con poco peso en bits. En estos casos queremos evitar tomar demasiadas muestras por segundo, si sabemos que la frecuencia máxima que genera la voz humana está alrededor de 3KHz, entonces podemos usar el teorema de Nyquist-Shannon que nos dice que nuestra mínima frecuencia de muestreo debe ser de 6.000 muestras por segundo. Sin embargo, debemos pensar que es probable que el micrófono que captura la voz humana, acepte frecuencias más altas de 3KHz, en este caso obtendremos aliasing ya que el máximo no será 3.000 sino lo que capture el micrófono. Para evitar este efecto, debemos asegurarnos que la máxima frecuencia sea la 203 determinada por el teorema, esto quiere decir que debemos usar filtros para no permitir pasar frecuencias por encima de 3.000Hz para nuestro ejemplo anterior. Como resultado de la discusión anterior, la mínima frecuencia de muestreo para una captura de todo el rango audible debe ser de 40.000 muestras por segundo. Como existen frecuencias superiores a 20.000Hz que podrían llegar a ser capturadas, debemos proteger el sistema usando filtros. Recordemos que esta es la base para proteger la captura, pero hablando de calidad la historia es otra. Entre más alta sea la frecuencia de muestreo, más fiel va a ser la representación de las ondas. Normalmente, en audio profesional se usan frecuencias de muestreo desde 44.100 muestras por segundo, hasta números mucho más elevados, sin embargo, cuando estamos creando aplicaciones de sólo voz, podemos llegar a usar frecuencias de muestreo de 8.000Hz. El API de sonido de Java está diseñado para manejar frecuencias de muestreo entre 8.000 y hasta 48.000 muestras por segundo. Si deseas manejar números mayores podrías llegar a crear tus propias clases con esta capacidad, pero ese tema excede los límites de este proyecto de grado. Un solo archivo de audio puede tener varios canales para transmitir diferente información relacionada o no, que permite generar la sensación de panorama auditivo. Podemos tener un mismo archivo de audio que bajo una misma frecuencia de muestreo capture 1, 2 o más canales de información de audio. En audio denominamos cuadros o frames al grupo de valores tomados en un mismo momento. En una muestra para un archivo estéreo de 16 bits, un cuadro o frame almacena 32 bits, 16 para un canal y 16 para el otro. Hoy día se hacen muy populares los archivos de audio que se reproducen en 5.1 y 7.1. En Java podemos crear aplicaciones capaces de manejar este tipo de archivos, sin embargo el API de sonido que viene con Java no es capaz por sí solo de aceptar y entender estos formatos, solamente acepta mono y estéreo. 204 Una vez tenemos todos nuestros frames almacenados, debemos guardarlos en archivos de audio. Una serie de bits de audio pueden almacenarse con una codificación específica, esto significa la manera en que guardamos los bits, que en ocasiones permite comprimir sin pérdidas de información, y en otras ocasiones genera pérdidas en la información pero de tal forma que el resultado siga siendo muy parecido al original o al menos aceptable para el oyente. La codificación no es más que una serie de algoritmos que nos permiten ordenar en cierta forma los bits que representan las ondas de un audio. Estas codificaciones las llamamos códec y el más famoso es PCM 'Pulse Code Modulation' que mantiene la integridad de los datos. El API de sonido de Java soporta los siguientes tipos de codificación para audio A-LAW, U-LAW, PCM SIGNED y PCM UNSIGNED. Cuando vemos las palabras signed y unsigned se refiere a los valores que representa un byte. Cuando es signed representa valores desde -128 hasta 127 siendo 0 el centro, cuando es unsigned, un byte representa los valores del 0 al 255 siendo 128 el centro. Además de la codificación también tenemos los archivos en sí, contenedores o formato de audio que puede entenderse muy fácilmente si pensamos en un archivo .mov de QuickTime de Apple que permite una serie de codificaciones diferentes para el audio, pero sin importar la codificación, todos los guarda dentro de un archivo con extensión .mov. Por ejemplo existe el formato WAV que permite diferentes tipos de codificación pero por lo general se le ve en PCM. Los formatos soportados por el API de sonido de Java son WAV, AIFF, AU, SND y AIFF-C. Si bien el mundo del audio trata todos los días de presionar los límites de hasta dónde pueden llegar los sistemas manejando audio, esto no quiere decir que Java evolucione de la misma forma. Si bien el API de sonido por sí solo nos limita un poco en cuanto a lo que podemos hacer, existen varias formas para que nosotros mismos podamos crear aplicaciones capaces de casi cualquier cosa. Por ejemplo el API de sonido en Java no puede leer archivos mp3 por sí solo 9, lo cual es una 9 Aunque el API de audio de Java no pueda manejar mp3, para reproducción rápida de este tipo de archivos podemos usar APIs que encontramos en la web para bajar, incluso gratis algunos de ellos, que nos permiten la reproducción e integración de este tipo de archivos. 205 deficiencia grande si estamos trabajando en archivos livianos en la red, pero esto no significa que sea imposible usar mp3 en Java, simplemente significa que el camino más fácil no está disponible, lo que podemos hacer es crear nuestro propio API cuya función sea leer archivos mp3. Para lograr un API de este estilo debemos saber trabajar en el nivel más bajo de la escala de programación, esto quiere decir en el nivel de los bits, afortunadamente el API de sonido en Java nos permite trabajar a muy bajo nivel. "The Java Sound API specification provides lowlevel support for audio operations such as audio playback and capture (recording), mixing, MIDI sequencing, and MIDI synthesis in an extensible, flexible framework"(Java Sound API, 2010). La forma en que se ordenen los bytes al almacenarse dependen también de la arquitectura del ambiente en el que se esté trabajando. Ciertos sistemas guardan los bytes de una forma y otros de otra forma. Pensemos que queremos representar el número 1 en un byte. El resultado sería 00000001. En el mundo del audio, para representar una amplitud unsigned de 1, si estamos usando una profundidad de 16 bits, necesitamos 2 bytes para almacenar ese valor, por lo tanto en binario de 16 bits el número 1 es 00000000 00000001. Al primer byte se le conoce como MSB Most Significant Byte y al segundo se le conoce como LSB Least Significant Byte, este nombre se da porque si cambiamos un bit en el MSB el cambio en el dato es enorme, en cambio si se cambia un bit en el LSB el cambio es mucho más pequeño en la muestra. En programación también se tiende a llamar MSB y LSB no sólo para bytes sino para bits también y representan el bit de más a la izquierda y el bit más a la derecha respectivamente. Volviendo al ejemplo hay ciertos sistemas que guardan almacenan los bytes empezando por el MSB y luego el LSB, esto quiere decir 00000000 00000001 para el número 1, a esta forma de ordenar se le conoce como big-endian. Otros sistemas almacenan primero el LSB y luego el MSB, esto quiere decir 00000001 00000000 para el número 1, a este sistema se le conoce como little-endian. Aunque Java permite modificar los bits a nuestro antojo, su estructura guarda la información en big.endian y por eso en este formato es más rápido. 206 Explorando los recursos del sistema Cuando vamos a crear aplicaciones que manejen audio, lo primero que tenemos que tener en cuenta es el ambiente bajo el cual se va a ejecutar la aplicación. Con esto me refiero a que un usuario puede tener dos tarjetas de sonido instaladas en su sistema, en su configuración puede tener habilitado el micrófono de una tarjeta y la salida de audio puede estar habilitada para la otra tarjeta. Si estas son las preferencias del usuario, no debemos cambiarlas a menos que tengamos una razón fuerte para hacerlo. Dentro del API de audio de Java podemos manipular por dónde sale o entra el sonido, si por ejemplo alguien tiene activado solo los audífonos y no la salida por parlantes, aunque podemos cambiar estas preferencias del usuario, se considera un muy mal comportamiento por parte del programador, llegar a entrometernos con las decisiones de los demás. Es por esta razón que es indispensable conocer los recursos básicos del sistema de cada usuario. Así como vimos en MIDI, cada ambiente de trabajo en cada sistema puede ser muy distinto, debemos tratar de hacer aplicaciones lo suficientemente genéricas para que el usuario escoja sus preferencias cuando la aplicación sea demandante o al menos crear aplicaciones lo suficientemente amplias para funcionar en la gran mayoría de entornos. En este capítulo nos enfocaremos en cómo obtener los recursos del sistema, pero todavía no haremos nada útil con ellos. Así como en MIDI teníamos MidiSystem para acceder a varias funciones básicas en la creación de aplicaciones MIDI, en audio tenemos AudioSystem y su función es muy parecida a la de MidiSystem. La clase AudioSystem tiene un método llamado getMixerInfo() que devuelve objetos del tipo Mixer.Info que es una clase interna de la interfaz Mixer. Los objetos Mixer.Info son instancias que representan los dispositivos de audio instalados en nuestro sistema. El siguiente código nos muestra en la ventana de salida el nombre de dichos dispositivos: import javax.sound.sampled.*; 207 public class Main { public static void main(String[] args) { Mixer.Info[] infos = AudioSystem.getMixerInfo(); for(Mixer.Info info: infos) { System.out.println(info.getName()); } } } Recordemos que para usar el API de sonido, primero debemos importar el paquete correspondiente que es: javax.sound.sampled, en este caso estamos importando todas sus clases usando el signo *. He creado una variable llamada infos que es la encargada de contener el arreglo que devuelve AudioSystem.getMixerInfo(). Luego hacemos un ciclo sobre este arreglo para usar el método getName() de Mixer.Info que nos devuelve el nombre del dispositivo instalado. La siguiente imagen muestra la ventana de salida para este código en mi sistema: Todos los que empiezan diciendo Port, son puertos físicos tanto entradas o salidas del sistema. El resto de los ítems de la lista son un Mixer. Cuando pensemos en un Mixer no podemos tener nuestra concepción típica de una consola ya que en 208 Java este término es bastante flexible. Un Mixer según Java podría ser la entrada de micrófono, la salida de sonido, el software de audio de Java o cualquier dispositivo de audio. Antes de continuar, debo aclarar que en este punto la información que nos brinda tanto Java como el resto de documentación disponible, se vuelve confusa y contradictoria. Así como Java nombra cualquier dispositivo un Mixer, vamos a seguir encontrando terminología confusa. No por esto vamos a detenernos en el camino. A partir de este punto pretendo crear aplicaciones sencillas que demuestren de forma clara la implementación del API de audio. Como dije en la introducción, en Java podemos crear un editor de audio como Pro Tools, sin embargo crearlo sería muy complicado, lograrlo requeriría conocimientos avanzados y una amplia experiencia programando. La mejor aproximación para dar los primeros pasos es simplemente crear códigos sencillos, luego por tu cuenta puedes explorar a fondo el API para ver cuáles son sus límites. Para saber qué podemos hacer con cada uno de los elementos de la lista, debemos entender que Java tiene la siguiente estructura de interfaces: El mapa representa la herencia de 7 de las 8 interfaces que tiene el API de audio. En la parte superior encontramos Line que es una interfaz que representa un conducto que lleva audio. Tiene métodos como open() y close() que nos permiten abrir y cerrar una línea de audio, permitiendo su uso en la aplicación. No podemos crear infinitas líneas de audio sino las que nos permita el sistema, es por eso que cuando no estemos usando una línea debemos cerrarla. Abrir una línea nos 209 permite obtener sus recursos. La interfaz Port es para los ítems de la lista de nuestro ejemplo anterior que empezaban su nombre con la palabra Port, que no son nada más que entradas y salidas físicas. La interfaz Port no tiene métodos, por herencia podemos usar todos los de Line para cerrar y abrir el puerto seleccionado. La interfaz Mixer es para dispositivos de audio con al menos una línea de audio. Un Mixer no necesariamente se usa para mezclar un sonido, es por esta razón que la terminología empieza a ser enredada, porque aunque se llame Mixer, puede ser la entrada de micrófono sin necesidad de ser un Port. Si observas detenidamente la lista anterior de los dispositivos instalados en mi sistema, encontrarás en el tercer puesto un Mixer llamado 'Microphone (Realtek High defini', y en el puesto 11 aparece nuevamente pero como un puerto 'Port Microphone (Realtek High Defini'. Uno debe ser tratado como Mixer y el otro como Port, pero más adelante veremos cómo hacer eso, por ahora sigamos entendiendo la estructura de las interfaces. Como un Mixer también puede llegar a funcionar como una verdadera consola virtual, esta interfaz tiene métodos para obtener entradas y salidas e incluso si es posible sincronizar varias líneas. En el punto de las entradas y salidas de un Mixer debemos detenernos para enfocarnos en ciertos términos que pueden llegar a ser confusos. Java usa el término 'source' o fuente en español para entradas a un Mixer, y 'target' u objetivo en español para las salidas. Hasta aquí todo normal. Más adelante veremos que un 'source' también sirve para reproducir audio en los parlantes como si fuera una salida, aunque esto parece no tener mucho sentido, se da debido a la terminología confusa que usa Java, pero podemos entenderlo si pensamos que la salida de los parlantes en sí misma es un Mixer, por lo tanto para escribir datos en ese Mixer, necesitamos una fuente o 'source' para reproducir sonidos. Para mantenernos claros debemos pensar que Java toma cada dispositivo como un Mixer: la entrada de micrófono, la salida de los parlantes, etc. En Java debemos crear fuentes 'source' para escribir datos y objetivos 'target' para leer datos desde cualquiera de estos Mixer. Un source es capaz de escribir datos pero no lee, mientras que un target es capaz de leer datos pero no escribe. 210 DataLine es la interfaz que encierra todo lo relacionado directamente con el flujo de datos. Por ejemplo tiene métodos para empezar start() o parar stop() el flujo de audio. Incluso tiene un método para saber en qué frame vamos de la transmisión de datos: getLongFramePosition(). Las tres subinterfaces de DataLine son SourceDataLine para fuentes, TargetDataLine para objetivos y Clip para audio que no sea en tiempo real. Como puedes darte cuenta por la explicación pasada SourceDataLine escribe bytes, por lo tanto es una entrada para un Mixer de Java, mientras que TargetDataLine lee bytes y por eso funciona como una salida de un Mixer. Clip por su parte nos permite manejar audio completo almacenado en el sistema, por ejemplo un archivo de audio que tengamos guardado. Entre sus métodos encontramos loop() que nos permite hacer ciclos sobre una parte del audio, escogiendo los puntos de comienzo y final usando setLoopPoints(). Con la interfaz Clip podemos ir a un punto específico de un archivo de audio para reproducirlo desde allí usando setFramePosition(). Hasta aquí he hecho una breve descripción de la forma en que están organizadas las interfaces que usamos cuando estamos usando el API de audio en Java. Con los anteriores conocimientos no pretendo que puedas todavía programar nada, sino estructurar tu pensamiento hacia la forma en que se organiza el API. Para poder seguir es clave que entiendas que cualquier dispositivo relacionado con el audio puede ser un Mixer, y que cada uno de estos Mixer necesita entradas o salidas, esto quiere decir fuentes y objetivos. La entrada de micrófono es un Mixer, por lo tanto para enviar la información a nuestra aplicación necesita un target. La salida de parlantes es un Mixer, por lo tanto necesita recibir información mediante un source. Hasta ahora sabemos obtener una lista de los dispositivos de audio instalados en el sistema. Supongamos que quiero usar el cuarto elemento de mi lista de dispositivos, este es un Mixer para Java y es la entrada de micrófonos de una MBox 2 pro. Por ahora no hagamos nada realmente útil con esa información proveniente de la MBox, solo vamos a aprender a crear un TargetDataLine para 211 ese Mixer, que sea capaz de transportar esa información, en el siguiente capítulo te enseñaré a hacer algo útil con la información proveniente del micrófono. El siguiente código está diseñado para ser agregado al código que nos imprime en la ventana de salida la lista de dispositivos de audio instalados en el sistema, debemos escribirlo dentro de main() y no veremos ningún cambio al compilar y ejecutar la aplicación, su función es demostrar cómo usar uno de los Mixer de la lista, en este caso el cuarto elemento que es el índice número 3, además crea un TargetDataLine de dicho Mixer: try { TargetDataLine mic = AudioSystem.getTargetDataLine(new AudioFormat(44100, 16, 1, true, true), infos[3]); }catch(Exception e){ System.out.println(e); } Para usar un dispositivo de la lista debemos empezar pensando si dicho Mixer o Port necesita un SourceDataLine o un TargetDataLine, en este caso estamos usando un Mixer que es una entrada de micrófono, las entradas físicas a nuestras interfaces en general necesitan un TargetDataLine que son las líneas encargadas de leer la información que nos provee un Mixer. Como podemos ver en el código anterior, para poder crear un TargetDataLine, podemos ayudarnos de AudioSystem y su método getTargetDataLine() que recibe dos argumentos. El primero es un objeto de tipo AudioFormat, encargado de crear el tipo de codificación que vamos a usar para capturar el audio del micrófono, el segundo parámetro es un objeto de tipo Mixer.Info que indica el Mixer que vamos a usar para crear dicho TargetDataLine. Existen ciertos Port y Mixer que no dejan crear un TargetDataLine, o si por ejemplo ya hemos creado demasiadas líneas de un mismo Mixer, obtendremos una excepción, por eso rodeamos todo en un try-catch. En realidad podríamos hacer este código anterior un poco más robusto para 212 asegurarnos que el dispositivo seleccionado pueda crear un TargetDataLine, sin embargo quiero mantener el código lo más simple por ahora. Un objeto del tipo AudioFormat se puede crear partiendo de tres constructores distintos. En este caso he usado el siguiente constructor: new AudioFormat(44100, 16, 1, true, true) Este constructor usa codificación PCM lineal. El primer argumento es la frecuencia de muestreo, el segundo es la cantidad de bits que vamos a usar para cada muestra, el tercero es la cantidad de canales, el cuarto argumento indica si se va a usar información signed, recordemos que esto quiere decir que cada byte representa valores entre -128 y 127, el último argumento indica si es big-endian. Los otros constructores nos permiten escoger otro tipo de codificación como ALAW o U-LAW. Aunque el código anterior aparentemente no hace nada, es el punto de partida para recibir la información del micrófono. En el siguiente capítulo veremos cómo a través del método read() de TargetDataLine, podemos leer la información y luego grabarla u oírla en los parlantes. El primer elemento de la lista de dispositivos de audio es 'Java Sound Audio Engine'. Este Mixer soporta 32 SourceDataLine y 32 Clip, no soporta ningún TargetDataLine. Para obtener este Mixer en mi lista usaría el siguiente código: Mixer mixer = AudioSystem.getMixer(infos[0]); Con el código anterior ya podríamos usar métodos de Mixer sobre la variable de referencia mixer. Por ejemplo podemos crear un SourceDataLine y un Clip del Mixer anterior: 213 SourceDataLine fuente1 = (SourceDataLine) mixer.getLine(new Line.Info(SourceDataLine.class)); Clip clip1 = (Clip) mixer.getLine(new Line.Info(Clip.class)); Tanto para SourceDataLine como para Clip usamos el mismo proceso. Usamos el método getLine() de Mixer que recibe como argumento un Line.Info que obtenemos mediante el constructor new Line.Info() que recibe la clase de la interfaz de la cual estamos hablando, esto quiere decir que para SourceDataLine usamos el nombre de la interfaz y con sintaxis de punto le agregamos la palabra clave class, por ejemplo SourceDataLine.class y el respectivo para Clip sería Clip.class. El método getLine() devuelve un objeto de tipo Line que por polimorfismo podemos convertir al tipo correcto usando un cast. Ya sabemos cómo obtener un Mixer, sabemos cómo crear los tres tipos de DataLine, pero aún no hemos visto cómo usar un Port. El único fin de usar un Port en una aplicación es para asegurarnos que el sonido esté saliendo o entrando por éste y en algunos casos controlar ciertos parámetros como el volumen de un puerto. Debemos ser cuidadosos porque el usuario puede tener desactivado un Port por razones de privacidad o para no molestar las personas cerca. Si abrimos un puerto sin que el usuario lo haya determinado, estamos incurriendo en un acto hostil. Lo mejor es sólo trabajar con puertos bajo interacciones específicas del usuario, por ejemplo crear un menú de preferencias de audio de la aplicación, donde el usuario puede seleccionar el puerto que desee, sólo entonces deberíamos cambiar el estado de un puerto. No podemos crearle a un puerto una línea, solo podemos usar los puertos para abrirlos, cerrarlos y usar sus controles. Al abrir un puerto sólo nos queda esperar que la información viaje por allí, pero como no podemos crearles líneas, no son una forma de obtener datos o enviar datos directamente. Los controles de un puerto varían dependiendo de cada hardware, normalmente podemos silenciarlos y cambiar su volumen, sin embargo esto depende de cada uno y el manejo de cada control lo dejo para que por tu cuenta vayas y explores el API. El siguiente código utiliza el quinto elemento en mi 214 lista de dispositivos, que aunque puede ser tratado como un Mixer, no se le pueden crear fuentes ni objetivos ya que es un Mixer que está dedicado a manejar únicamente su Port: Mixer mixer = AudioSystem.getMixer(infos[4]); Line.Info[] puertos = mixer.getTargetLineInfo(new Line.Info(Port.class)); for(Line.Info puerto: puertos){ Port unPuerto = (Port) mixer.getLine(puerto); unPuerto.open(); } El código anterior crea un Mixer a partir del quinto elemento en mi lista de dispositivos. Usamos el método getTargetLineInfo() que recibe un Line.Info para poder acceder a todos sus puertos. En este caso es un puerto de salida y por eso debemos usar getTargetLineInfo, aunque antes usáramos la salidas con fuentes, para los puertos usamos objetivos, pero si fuera una entrada deberíamos usar getDataLineInfo. Es por este tipo de contradicciones en el API de sonido de Java que a veces podemos confundirnos. Luego hacemos un ciclo sobre el arreglo para obtener el único puerto que tiene este Mixer y así abrirlo. Si bien esta es la forma de obtener un Port, el API no nos dice mucho al respecto. La experiencia me ha enseñado que debemos pensar un Port en Java como una forma de ir y controlar directamente si permitimos o no el paso de señal por una entrada o salida física, pero no podemos ir directamente y usar los puertos para obtener información y al abrir un puerto de salida sólo podemos esperar que el audio realmente salga, pero más allá de eso no podemos hacer mucho. Para aplicaciones rápidas que no necesiten experimentar tanto con los recursos del sistema, podemos aprovecharnos de AudioSystem que nos ayuda a obtener los recursos por defecto del sistema. Supongamos que queremos escribir en la salida de audio que por defecto tenga el sistema, para eso podemos crear un SourceDataLine de la siguiente forma: 215 AudioFormat format = new AudioFormat(44100.0F, 16, 1, true, true); DataLine.Info sourceInfo = new DataLine.Info(SourceDataLine.class, format); sourceDataLine = (SourceDataLine) AudioSystem.getLine(sourceInfo); Si quisiéramos capturar el micrófono predeterminado del sistema, debemos cambiar los SourceDataLine por TargetDataLine y listo. Con este corto código obtenemos en sourceDataLine la referencia a los datos de audio. En el siguiente capítulo aprenderemos a usarlos para grabar, reproducir y capturar. Si lo quisieras, también podrías modificar el código para crear un Clip sin pensar en los recursos del sistema. Con sólo ver la lista de dispositivos instalados o disponibles en el sistema no es suficiente para saber cuáles son exactamente entradas o salidas, a cuáles les podemos crear SourceDataLine, a cuáles TargetDataLine ni a cuáles Clip. El primer paso es hacer la lista un poco más robusta para saber cuáles pueden crearse sobre cada Mixer: import javax.sound.sampled.*; public class LearningAudio{ public static void main (String[] args) { Mixer.Info[] mixerInfo = AudioSystem.getMixerInfo(); Line.Info sourceDataLineInfo = new Line.Info(SourceDataLine.class); Line.Info targetDataLineInfo = new Line.Info(TargetDataLine.class); Line.Info clipInfo = new Line.Info(Clip.class); String texto; Mixer mixer; for(int c = 0; c < mixerInfo.length; c++){ texto = ""; System.out.println(mixerInfo[c].getName()); mixer = AudioSystem.getMixer(mixerInfo[c]); 216 if (mixer.isLineSupported(sourceDataLineInfo)) { texto += " SourceDataLine = " + mixer.getMaxLines(sourceDataLineInfo) + "."; } if (mixer.isLineSupported(clipInfo)) { texto += " Clip = " + mixer.getMaxLines(clipInfo) + "."; } if (mixer.isLineSupported(targetDataLineInfo)) { texto += " TargetDataLine = " + mixer.getMaxLines(targetDataLineInfo) + "."; } System.out.println(texto); } } } El código anterior es muy simple y nos muestra en la ventana de salida qué Mixers pueden tener un SourceDataLine, cuáles TargetDataLine y cuáles Clip. Primero creamos los tres tipos de Line.Info pasándole a su constructor la clase que buscamos. Luego hacemos un ciclo sobre cada Mixer para obtenerlo y usando el método isLineSupported() para cada tipo de línea miramos si el Mixer es capaz con dicha línea, si lo es, usamos el método getMaxLines() para saber cuántas se pueden crear. Sobre los Mixer que representan puertos no obtenemos nada, sobre los otros obtengo en mi sistema el siguiente resultado: 217 Cuando obtenemos -1, quiere decir que podemos crear tantas líneas como nuestro sistema, procesador y memoria lo permitan. Si bien hasta este punto no hemos hecho nada interesante con los métodos del API de sonido, tener claro estos primeros pasos es indispensables para poder programar cualquier aplicación de audio. Hasta aquí no he creado códigos robustos ya que la idea de estas primeros usos del API no están destinados a programar de la forma más robusta, sino a entender la forma en que está diseñada la estructura de las clases e interfaces de audio en Java. A partir del siguiente capítulo empezamos a crear códigos más útiles, pero antes quiero cerrar este capítulo con una mirada general a lo que no se puede dejar pasar. Java nombra como Mixer cualquier dispositivo de audio instalado en el sistema. Podemos obtener los Mixer y sus líneas usando métodos de AudioSystem que es una clase muy útil en toda aplicación de audio, incluso más adelante nos va a servir para escribir y guardar archivos de audio en nuestro computador. Para poder usar estos Mixer necesitamos crear líneas que no son más que subinterfaces de DataLine. Estas subinterfaces pueden ser fuentes SourceDataLine, objetivos TargetDataLine o Clip. Las fuentes escriben y los objetivos leen, por eso una fuente es una entrada a un Mixer y un objetivo es una salida del mismo. Los Clip son casos especiales para audio guardado en el sistema. Si la aplicación es compleja tal vez desees entrar a explorar a fondo los recursos del sistema, sin embargo para aplicaciones sencillas lo más útil es dejar que AudioSystem busque los recursos predeterminados. Antes de continuar ve y busca la documentación del API de sonido para que veas qué dice sobre los métodos que hemos usado aquí y cuál es la descripción que aparece de cada interfaz y clase usada. La clave de este capítulo es que sepas cómo crear TargetDataLine, SourceDataLine y Clip escogiendo el Mixer o Port que desees de la lista de tu sistema. 218 Capturar, grabar y reproducir En el capítulo pasado exploramos de forma extensa los recursos del sistema. Para lograr capturar, grabar y reproducir vamos a necesitar usar los tres tipos de DataLine. Para facilitar el proceso usaremos los predeterminados por el sistema usando AudioSystem. Como ya dije antes, un TargetDataLine lee información. Cuando creamos uno predeterminado en el sistema debemos primero abrirlo usando el método open() que recibe dos argumentos: el formato del audio y el tamaño del buffer en bytes que debe corresponder con un número entero de frames. Luego usamos el método start() para empezar a recibir información. Después podemos usar el método read() para capturar la información que entra por el micrófono predeterminado. Este método necesita tres argumentos: 1. Un arreglo de tipo byte cuyo tamaño debe corresponder con el tamaño de un número entero de frames para evitar distorsiones o interrupciones en el audio. Si estamos usando una señal mono a 16 bits, entonces el arreglo debe ser de un largo de un número par de bytes. Si el formato es estéreo y la profundidad es de 16 bits, entonces el largo del arreglo debe ser un múltiplo de 4. Este byte se usa para poder procesar la información por partes. En este arreglo se almacenará la información para poder ser leída por partes. Si el largo de este arreglo es muy largo, la latencia será alta, si lo hacemos muy corto, corremos el riesgo de que el sistema no sea capaz de manejar la información tan rápidamente. Mi mejor consejo es siempre probar con varios valores, ojalá usando un computador promedio como el que los usuarios de la aplicación puedan tener. 2. Un entero que indica el desplazamiento en bytes del arreglo. Por lo general es cero. 3. Un número entero que indica el total de bytes a leer, lo natural es indicar el largo del arreglo. 219 La siguiente aplicación es muy simple pero demuestra cómo capturar audio de un micrófono y luego grabarlo en un archivo en el computador: import javax.sound.sampled.*; import javax.swing.*; import java.awt.event.*; import java.io.File; public class Main implements ActionListener{ boolean grabar = false; TargetDataLine targetDataLine; AudioFormat format = new AudioFormat(44100, 16, 1, true, true); JButton boton; public static void main(String[] args) { Main test = new Main(); test.gui(); } public void gui() { JFrame frame = new JFrame("Amplificación en Java."); frame.setLayout(null); boton = new JButton("Grabar"); frame.getContentPane().add(boton); boton.setBounds(50, 75, 200, 100); boton.addActionListener(this); frame.setSize(300, 300); frame.setVisible(true); frame.setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE); } public void actionPerformed(ActionEvent event) { if(grabar == true) 220 { boton.setText("Grabar"); targetDataLine.stop(); targetDataLine.close(); targetDataLine.flush(); grabar = false; } else { boton.setText("Detener"); go(); } } public void go() { grabar = true; DataLine.Info targetInfo = new DataLine.Info(TargetDataLine.class, format); try { targetDataLine = (TargetDataLine) AudioSystem.getLine(targetInfo); Thread audio = new Audio(); audio.start(); } catch(Exception e) { System.out.println(e); } } class Audio extends Thread { byte[] temp = new byte[targetDataLine.getBufferSize() / 1000]; AudioFileFormat.Type tipo = AudioFileFormat.Type.AIFF; File archivo = new File("grabacion.aif"); public void run() { 221 try { targetDataLine.open(format); targetDataLine.start(); AudioSystem.write(new AudioInputStream(targetDataLine), tipo, archivo); } catch (Exception e){ System.out.println(e); } } } } Lo que está ocurriendo es muy sencillo, no hay que preocuparse por ver tanto código. Primero creamos las variables que vamos a usar en más de un método y por eso las creamos fuera de todo método. Luego dentro de main() creamos un objeto de la misma clase para poder llamar el método gui() que va a ser el encargado de crear la parte visual que puedes analizar por tu propia cuenta, es sólo un botón. La clase implementa ActionListener ya que por ser un sólo botón no es necesario crear una clase interna para manejar el evento, lo podemos manejar dentro de esta misma clase en el método de Java actionPerformed(), cuando presionamos el botón este método se dispara cambiando el texto del botón a 'Detener' y además se llama el método go(). Dentro de go() creamos un TargetDataLine como aprendimos en el capítulo pasado. Normalmente en casi todas las aplicaciones de audio, es buena idea crear un nuevo Thread que se encargue exclusivamente del manejo del audio para permitir la continuidad de la aplicación. Dentro del nuevo hilo he creado el arreglo tipo byte que necesita el método read() y además he creado una variable que contiene el tipo de archivo en que vamos a guardar el audio. en este caso es un archivo AIFF, pero bien pudo ser WAVE, SND, AU o AIFC. Por último creé una variable llamada archivo del tipo File para decidir el nombre y ubicación relativa del audio guardado. El objeto File se encuentra en el paquete java.io. 222 Finalmente, dentro del método run() abrí el TargetDataLine mediante la versión del método open() que no necesita especificar tamaño del buffer, después usé el método start() para empezar a capturar la información y por último usé el método write() de AudioSystem que necesita tres argumentos: una nueva instancia de un objeto AudioInputStream que recibe en su constructor un TargetDataLine, el tipo de formato de archivo y la ubicación del mismo. Cuando presionamos el botón para detener la grabación se disparan los métodos stop(), close() y flush que liberan los recursos de la línea y desocupan el buffer. El archivo queda guardado en la carpeta del proyecto de NetBeans. Ya sabemos capturar sonido y guardarlo en un archivo. Ahora aprendamos el proceso contrario, leer un archivo y reproducirlo. Usemos el audio guardado en el ejemplo anterior llamado 'grabacion.aif' que debemos poner en la carpeta madre que contiene todo el siguiente proyecto: import javax.sound.sampled.*; import java.io.File; public class Main { SourceDataLine fuente; AudioFormat formato; AudioInputStream ais; public static void main(String[] args) { Main main = new Main(); main.empezar(); } public void empezar() { File archivo = new File("grabacion.aif"); try{ ais = AudioSystem.getAudioInputStream(archivo); formato = ais.getFormat(); 223 DataLine.Info dataInfo = new DataLine.Info(SourceDataLine.class, formato); fuente = (SourceDataLine) AudioSystem.getLine(dataInfo); Thread audio = new Audio(); audio.start(); }catch(Exception e){ System.out.println(e); } } class Audio extends Thread { byte[] arreglo = new byte[10000]; public void run() { try{ fuente.open(formato); fuente.start(); int cuenta; while((cuenta = ais.read(arreglo, 0, arreglo.length)) != -1){ if(cuenta > 0){ fuente.write(arreglo, 0, cuenta); } } }catch(Exception e){ System.out.println(e); } } } } Si analizas el código te darás cuenta que es mucho más sencillo de lo que parece. En main() estamos creando un objeto de la clase que contiene todo el código para así poder llamar otros métodos y además poder crear una clase interna para el 224 nuevo hilo. Recordemos que el código principal encargado de la lectura y escritura de audio, debe siempre hacerse en un nuevo hilo. Si te das cuenta, el código es muy parecido a la lectura del micrófono, la diferencia radica en el ciclo while que simplemente dice 'mientras haya algo que leer en el AudioInputStream, me mantengo en el ciclo'. Un objeto AudioInputStream sirve para mantener un canal de información que se envía, y se necesita para leer la información de un archivo o de un micrófono o entrada de audio. Su método read() devuelve -1 cuando ha terminado de leer el contenido, por eso mantenemos el ciclo mientras no devuelva -1. El código anterior no usa un GUI, por lo tanto no podemos controlar la reproducción. El ejemplo demuestra la forma de cargar un archivo para streaming de audio. Sin embargo, con los conocimientos que te he dado hasta ahora puedes buscar la forma de crear un Clip para mantener allí el AudioInputStream y así poder crear un GUI que le permita al usuario controlar la posición y ejecución del audio. Por último, creo que la mejor forma de aprender a entender el API de audio es que te retes a ti mismo a crear una aplicación que reciba el audio del micrófono y luego lo reproduzcas en tiempo real en tus parlantes. Trata de usar audífonos para evitar feedbacks. Es posible que debas cambiar los buffer tanto del TargetDataLine como del SourceDataLine en el método open() que acepta el tamaño del buffer. También cambia el tamaño del arreglo para que veas cómo puede cambiar la latencia. Si tratas de construir esa aplicación, verás que empezamos a probar los límites de Java ya que el audio en tiempo real demanda un muy buen sistema. Yo he creado dicha aplicación y aunque no voy a enseñar a crearla en este proyecto de grado, ya que te he enseñado las bases para que tú mismo puedas hacerlo, voy a compartir mi experiencia y resultado de esta aplicación en las conclusiones al final del texto. 225 Una aplicación real A lo largo de este escrito hemos explorado el mundo de la programación en Java desde la perspectiva del audio. Sin embargo todos los códigos han sido tan sólo ejemplos básicos que nos ayudan a entender un punto específico de la programación, pero todos están lejos de ser considerados siquiera una aplicación. Es importante ver los ejemplos, pero en este punto en que has dado una mirada al lenguaje, es bueno que puedas sentarte en tu silla, relajarte y ver cómo trabajo creando una aplicación de la vida real. Esto te ayudará a estructurar tu forma de pensar cuando estés a punto de crear tus primeras aplicaciones. En mi empresa www.ladomicilio.com, continuamente estamos creando nuevas aplicaciones que nuestros usuarios puedan usar. Como dije al comienzo de este escrito, mis primeros pasos en la programación se dieron en AS3, lenguaje de Flash de Adobe. Sin embargo, en flash no podemos crear ciertas aplicaciones porque su precisión en el tiempo es muy pobre. En mi vida me encontré con Java porque descubrí que este lenguaje era la solución a los problemas de tiempo que me presentaba Flash. La falta de precisión de Flash la descubrí tratando de hacer un metrónomo, pues bien, en estos últimos capítulos quiero que puedas sentarte y disfrutar cómo es el proceso de creación de una nueva aplicación para La.Do.Mi.Cilio, que por razones obvias es un metrónomo. Mi forma de trabajar programando y creando una aplicación no quiere decir que es la última palabra ni la única forma de lograrlo. Seguramente alguien pueda lograr el mismo resultado con menos líneas de código, o tal vez alguien tenga un orden diferente al que nos vamos a enfrentar a continuación. La siguiente es la forma que más se me acomoda para que el resultado sea lo que espero. Mi consejo en la creación de toda aplicación es estar tranquilo, ser muy creativo, aprender a solucionar problemas sin desesperarse, tener el API a la mano, buscar ayuda en internet cuando nos sentamos perdidos, y cuando no veamos una solución cercana, lo mejor que podemos hacer es alejarnos del código, seguro después de 226 descansar nos demos cuenta que la solución había estado más cerca de lo que pensábamos. Para mi es una mala idea empezar una aplicación pensando qué es posible en el lenguaje y qué no, la mejor idea es siempre pensar en el usuario y no el programador. Si éste último tiene que sufrir en el proceso de creación no es problema, en cambio si todos los miles de usuarios tienen que sufrir por culpa de que un programador no sufrió un poco más, entonces descubriremos que nuestras aplicaciones no son tan apreciadas. Nunca podemos defender errores de programación explicando a los usuarios los límites de un lenguaje ya que a ellos poco debe importarles esto. Siempre debemos empezar y terminar una aplicación pensando como el usuario final y no como un programador. Si hay una aplicación que queremos lograr y de verdad descubrimos que es imposible en cierto lenguaje, no es necesario descartar la idea del todo, yo he tenido que aprender más de 4 lenguajes de programación entre otros códigos para poder lograr las aplicaciones en mi empresa. Si algo puedo decir de todos ellos es que Java es el más poderoso en cuanto al audio se refiere para poder trabajar en la web. Para otras aplicaciones más demandantes que no son para ser incrustadas en la web, podemos usar otros lenguajes todavía más completos y robustos para audio como lo puede llegar a ser C++, que tiene una ventaja sobre Java, y es que al compilar se obtiene lenguaje de máquina que es lo más rápido que se puede llegar y permite la menor latencia posible. Al final del camino, cuando terminamos una aplicación, descubriremos que hemos aprendido mucho del lenguaje, porque en cada nueva aplicación siempre hay un reto por superar, siempre hay dificultades, solo con el tiempo empezamos a darnos cuenta cómo podemos empezar a hacer códigos más rápidos, más robustos, más libres de errores y sobre todo, más reusables y sostenibles en el futuro. Sin más preámbulo, empecemos a programar un metrónomo para La.Do.Mi.Cilio. 227 Planeación EL primer paso antes de escribir un código alguno, es sentarse a pensar cómo va a ser la aplicación para el usuario, con todas sus características, sin pensar ni siquiera en la parte visual todavía. Debemos olvidar que sabemos algo de programación porque esto nos limitará lo que creemos que podemos hacer. Simplemente el primer paso es ser un usuario más de nuestra aplicación que aún no existe. En la planeación del metrónomo, empecé pensando cómo son la mayoría de metrónomos que ya existen, siempre debemos conocer nuestra competencia. De forma muy básica todos nos permiten escoger un tempo en bpm Beats Per Minute, nos permiten escoger cada cuánto queremos un acento y nos permiten prenderlo y apagarlo. Eso entre lo más básico. A mí me parece buena idea cuando tienen un control de volumen ya que esto nos permite ajustarlo de acuerdo a algo más que estemos oyendo, como nuestro instrumento. Con estas características tenemos la base para un metrónomo muy sencillo, que con el tiempo puede empezar a evolucionar y tener muchas otras características. Lo mejor que podemos hacer es empezar con aplicaciones sencillas, la experiencia me ha demostrado que cuando un usuario tiene un programa fácil, claro y simple, se siente más identificado con él. Un tiempo después puede salir la segunda versión del programa con más características y el usuario las va a ir asimilando a medida que vayan saliendo, pero una cosa es que el usuario evolucione con la herramienta, y otra cosa es que en la primera versión la aplicación ya sea súper compleja, en este caso nadie se tomará la molestia de usarla porque sabrán que no es fácil de usar. Es bueno que toda aplicación tenga algo que la haga especial, si bien hay muchas cosas que le podemos agregar al metrónomo, se me ocurren dos que van a ayudar a que este metrónomo tenga algo de más que lo haga verdaderamente útil. La primera es la posibilidad de escoger una subdivisión, muchas veces los músicos estamos estudiando y algunos metrónomos no permiten escoger la 228 subdivisión del tempo que tenemos escogido. Pero como bien sabemos, una subdivisión de por ejemplo una negra, no necesariamente tiene que ser de dos corcheas, puede que la subdivisión de una canción sea de tres o incluso de dos pero shuffle, que quiere decir cuando la primera de las dos corcheas que dividen una negra, se toma un poco más de tiempo que la segunda. La segunda adición que le vamos a poner al metrónomo es un botón de 'tap tempo' que nos permite escoger la velocidad del metrónomo presionando el botón con la rapidez o tempo que necesitemos. También necesitamos una porción de texto para indicarle al usuario que un error ha ocurrido, ya sea por parte de la aplicación o por un mal manejo del metrónomo por parte del usuario. Por último necesitamos alguna parte donde podamos poner texto por si el usuario necesita información sobre cómo usar el metrónomo. La siguiente es la lista completa de las características del metrónomo con la forma en que hemos decidido presentárselo al usuario: 1. Un botón de encendido y apagado. 2. Un campo que nos permite escribir la velocidad en bpm con un botón al lado que permita cambiar al nuevo tempo que hemos seleccionado. 3. Un campo que nos permita escoger cada cuántos pulsos queremos un pulso fuerte o acento, acompañado de un botón que nos permita cambiar al valor puesto en el campo de texto. 4. Una especie de menú desplegable que nos permita seleccionar si no queremos que nos marque una subdivisión, si queremos una división straight, si queremos una subdivisión shuffle o si la queremos ternaria. 5. Un botón de TAP TEMPO. 6. Un botón deslizable para el volumen. 7. Un campo de texto para informar al usuario sobre errores. 8. Un campo de texto para informar al usuario cómo debe usarse el metrónomo. A la lista anterior debemos sumar los requisitos que nos pone la empresa para la que estamos trabajando si es que los hay. En este caso, la forma en que está 229 diseñada la página www.ladomicilio.com, me obliga a sumar los siguientes requisitos: 9. El tamaño exacto debe ser de 400 pixeles de ancho por 335 pixeles de alto. 10. Aunque podemos poner los colores que queramos, los tres colores principales de la página son blanco, negro y rojo para mantenernos en el estilo. Debo aceptar también que para llegar a esta lista, he preguntado a músicos, amigos, familiares y usuarios de la página para entender qué quiere la gente, qué esperan de la aplicación y en varias ocasiones nos encontramos con ideas bien interesantes. Siempre es mejor empezar por la retroalimentación del usuario final, la mejor forma de crear una aplicación es empezar por el revés. Siguiendo con el revés, es una buena idea saber cómo se va a ver exactamente la aplicación. Como el espacio es tan reducido, no nos va a caber todo en la pantalla, para solucionarlo he decidido que todo debe verse en el espacio especificado menos la ayuda para el usuario, que debe aparecer solamente cuando el usuario hace clic sobre un botón que tiene la forma de un signo de interrogación. Para esto es probable que tengamos un grupo de diseño a parte que haga la parte visual y eso está bien porque muchas veces los programadores no son buenos con el diseño. Para el metrónomo hemos decidido que el aspecto de todos los botones menos uno va a ser el aspecto que nos brinda Java y el sistema operativo. El único botón que va a tener un diseño personalizado es el botón que tiene el signo de interrogación que sirve para que el usuario aprenda a usar el metrónomo, cuando el botón es presionado, desaparecerán todos los controles del metrónomo y aparecerá un texto con las explicaciones necesarias. Después de cierto trabajo con el equipo de diseño y algunos cambios, hemos decidido que el siguiente será el aspecto del metrónomo, como los botones cambian entre los diferentes sistemas operativos, escogimos los de Windows 7 como base para mostrar el resultado: 230 Cuando tenemos un equipo de diseño queremos que la aplicación quede igual a lo que ellos determinan. Normalmente ellos nos entregan una tabla con las coordenadas y tamaños exactos, pero no es raro que al final tengamos que hacer algunas modificaciones para que se vea tal y como nos muestran las imágenes. En la parte inferior del volumen, queda un espacio en donde aparecerá información cada vez que el usuario se equivoque o la aplicación reporte un error. La siguiente imagen muestra cómo se verá la ayuda de la aplicación: 231 Programando Manos a la obra. Existen patrones de diseño de aplicaciones y libros enteros que se dedican a cómo se debe organizar la programación para obtener mejores resultados. Antes de empezar debo aclarar que hay cientos de formas en que podemos mejorar el código y seguramente tengas mejores propuestas que las mías. Por ahora no quiero hacer clases súper complejas ni hablar de patrones de diseño, sino empezar por crear una clase que se llame Metronomo y que de pronto más adelante nos pueda ser útil. Normalmente para las aplicaciones no suelo escribir todo en una sola clase. Debido a que no me pareció una aplicación complicada de hacer, decidí hacer todo dentro de una única clase llamada Metronomo que va a contener el main() que ejecuta toda la aplicación haciendo un objeto de sí mismo. Hay dos cosas que quiero que ocurran cuando creamos el objeto: primero que se cree todo el GUI y segundo que se inicialice la aplicación. Esta es una aplicación MIDI que para empezar necesita crear un secuenciador y una secuencia, por lo tanto se me ocurre un diseño para la aplicación: import javax.sound.midi.*; import javax.swing.*; import java.awt.event.*; import javax.swing.event.*; import java.awt.*; public class Metronomo { // Aquí van todas las variables que se compartan entre distintos métodos. public static void main(String[] args) { Metronomo metronomo = new Metronomo(); } public Metronomo() { gui(); 232 empezar(); } private void gui() { } private void empezar() { } private void crearSecuencia(int tipo, int acento) { } static MidiEvent eventosMIDI(int status, int data1, int data2, int pulso, int division) { } public void setTempo(int bpm) { } private void tapTempo(long now) { } // Clases internas para eventos // Clases para GUI } De forma muy simple vamos a necesitar los siguientes métodos: - main(): Allí crearemos una instancia de la clase y eso será suficiente para llamar al constructor que permitirá que la aplicación empiece: - constructor Metronomo(): Llamará dos métodos que iniciarán la aplicación, uno se encargará del GUI y el otro se encargará de iniciar el secuenciador y la secuencia. Dentro del constructor podríamos escribir todo el código que hace ambas funciones, pero usar métodos separados nos ayudará a no revolver cosas que no tienen nada que ver y así mantener a futuro el código será mucho más fácil. - gui(): Es el método encargado de toda la parte gráfica, cada componente que permita acciones del usuario tendrá su propia clase interna para manejar el evento. - empezar(): Este método inicia un secuenciador y una secuencia de tal forma que todo queda listo para hacer sonar el metrónomo, pero no lo hace sonar hasta que el usuario lo decida. 233 - crearSecuencia(): Nos será muy útil este método para crear la secuencia correcta cada vez que queramos modificarla. Como vimos en capítulo de MIDI, crear una secuencia no es tan cómodo a menos que nos ayudemos de métodos que nos eviten reescribir código innecesariamente. Debido a que un metrónomo es cíclico, se me ocurre que este método puede usar los ciclos para poder crear las secuencias. Cada secuencia sólo tendrá que tener la duración de un compás ya que podemos usar algunos métodos del secuenciador que nos permiten mantenernos en un loop infinito. Se me ocurre que este método pida dos argumentos: el primero es el tipo de subdivisión y el segundo es cada cuántos pulsos queremos un acento fuerte. - eventosMidi(): Este es un método estático ya que es muy útil en muchas ocasiones, no sólo para un metrónomo. Crear un evento MIDI no es algo difícil pero hacerlo cada vez que necesitemos enviar un evento podría aumentar considerablemente nuestro código. Este método necesita 5 argumentos. El primero es el status byte, el segundo y el tercero son los data bytes, el cuarto es el número del pulso en el que queremos el evento, por ejemplo en el pulso uno, en el pulso dos, en el tercero, etc. El último argumento es la división, que es el número de ticks que debemos restar para poder crear las subdivisiones, gracias a este último argumento es que podemos generar eventos que no sean exactamente sobre el pulso. Por cierto la cantidad de ticks por pulso que he escogido es de 24 ya que es un número que no permite hacer subdivisiones tanto de corcheas como de tresillos y al ser un número grande podríamos en el futuro pedir eventos MIDI más complejos usando este método. - setTempo(): Es un método público que permite seleccionar un tempo en bpm. El número debe ser un entero entre 30 y 300, de lo contrario nada sucederá. Podríamos hacer que este método arrojara una excepción por si es usado más adelante en otros proyectos. Por ahora puede simplemente escribir en el texto de 234 errores para el usuario cuando se añaden letras o números fuera del rango o que no sean enteros. - tapTempo(): Este método recibirá el valor en milisegundos que tenga el sistema usando el método System.currentTimeMillis(). Al llamar este método por segunda vez se restará el argumento con el valor obtenido la vez anterior y el resultado se trasladará a un valor en bpm si está entre 30 y 300 para luego llamar el método setTempo(). Al comienzo del programa necesitamos crear 4 constantes cuyo nombre nos sea fácil de recordar para luego usar en el código. Estas definiciones van a servir para guardar el valor en ticks que necesitamos restar desde un pulso para crear una subdivisión: public final static int NO_DIVISION = 0; public final static int STRAIGHT = 11; public final static int SHUFFLE = 7; public final static int TERNARIO = 15; Como nuestra resolución es de 24 ticks por pulso, cuando queremos generar un evento en el primer tick debemos restar 23, para hacer una subdivisión STRAIGHT, debemos restar 11 ticks a 24, para SHUFFLE debemos restar 7 a 24 y así sucesivamente. Normalmente también hay una serie de variables que necesitamos usar en más de un método. Por ejemplo el campo de texto que dice errores al usuario, queremos que se actualice desde varias partes del código, para lograr esto debemos instanciarlo fuera de todo método. Las variables de instancia que van a necesitar ser usadas son: private Sequencer secuenciador; 235 private Track track1; private JTextField texto; private JLayeredPane fondo; private JButton botonStartStop; private JComboBox comboBox; private JSlider slider; private JLabel avisos; private JTextField textoAcento; private JPanel ayudaText; private long antes; private long ahora = 1; private int division; private int acentosCada; private int velocidad; private boolean enAyuda = false; Como podemos ver, la mayoría tienen que ver con la parte gráfica, esto se da porque por lo general queremos actualizar lo que ve el usuario. Por ejemplo cuando un tempo no ha sido modificado correctamente y el usuario vuelve a hacer clic sobre un botón, es buena idea que el campo se devuelva al valor actual del tempo correcto. El siguiente es el código completo del método que genera el GUI: private void gui() { JFrame marco = new JFrame("Metrónomo"); marco.setLayout(null); marco.setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE); marco.setSize(400, 357); marco.setIconImage(new ImageIcon(getClass().getResource("images/icon.png")).getImage()); marco.setResizable(false); 236 // JPanel para fondo Pintar pintar = new Pintar(); pintar.setBounds(0, 0, 400, 335); fondo = new JLayeredPane(); fondo.setBounds(0, 0, 400, 335); fondo.add(pintar, new Integer(0)); marco.setContentPane(fondo); // campo de texto para escribir el tempo texto = new JTextField("120"); texto.setBounds(40,88,100,20); texto.setHorizontalAlignment(JTextField.CENTER); fondo.add(texto, new Integer(1)); // botón setTempo JButton botonSetTempo = new JButton("Cambiar tempo (bpm)"); botonSetTempo.setBounds(194,87,170,20); fondo.add(botonSetTempo, new Integer(1)); botonSetTempo.addActionListener(new EventoTempo()); botonSetTempo.setCursor(new Cursor(Cursor.HAND_CURSOR)); // botón tapTempo JButton botonTapTempo = new JButton("Tap Tempo"); botonTapTempo.setBounds(210,204,170,20); fondo.add(botonTapTempo, new Integer(1)); botonTapTempo.addActionListener(new EventoTap()); botonTapTempo.setCursor(new Cursor(Cursor.HAND_CURSOR)); // botón start stop botonStartStop = new JButton("Iniciar"); botonStartStop.setBounds(152,29,100,20); fondo.add(botonStartStop, new Integer(1)); botonStartStop.addActionListener(new StartStop()); 237 botonStartStop.setCursor(new Cursor(Cursor.HAND_CURSOR)); // Combo box para escoger la subdivisión String[] lista = {"Sin división", "Straight", "Shuffle", "Ternario"}; comboBox = new JComboBox(lista); comboBox.setBounds(15,199,150,30); fondo.add(comboBox, new Integer(1)); comboBox.addActionListener(new Division()); comboBox.setCursor(new Cursor(Cursor.HAND_CURSOR)); // slider para volumen slider = new JSlider(JSlider.HORIZONTAL, 0, 127, 100); slider.setBounds(195,245,150,60); fondo.add(slider, new Integer(1)); slider.addChangeListener(new Volumen()); slider.setOpaque(false); // campo de texto para avisos de errores y otros avisos = new JLabel("", JLabel.CENTER); avisos.setForeground(Color.white); avisos.setBounds(0,298,400,30); fondo.add(avisos, new Integer(1)); // campo de texto para escribir el tempo textoAcento = new JTextField("4"); textoAcento.setBounds(40,147,100,20); textoAcento.setHorizontalAlignment(JTextField.CENTER); fondo.add(textoAcento, new Integer(1)); // botón setTempo JButton botonSetAcento = new JButton("Cambiar acento"); botonSetAcento.setBounds(194,145,170,20); fondo.add(botonSetAcento, new Integer(1)); botonSetAcento.addActionListener(new EventoAcento()); 238 botonSetAcento.setCursor(new Cursor(Cursor.HAND_CURSOR)); // Botón ayuda JButton ayuda = new JButton(new ImageIcon(getClass().getResource("images/help.png"))); ayuda.setBounds(358,6,30,30); fondo.add(ayuda, new Integer(2)); ayuda.addActionListener(new Ayuda()); ayuda.setCursor(new Cursor(Cursor.HAND_CURSOR)); ayuda.setBackground(Color.BLACK); // Panel de ayuda ayudaText = new TextoAyuda(); ayudaText.setForeground(Color.white); ayudaText.setBounds(0,0,400,335); ayudaText.setBackground(Color.black); fondo.add(ayudaText, new Integer(2)); fondo.setLayer(ayudaText, 0, -1); // La siguiente línea debe ir de último en todo GUI marco.setVisible(true); } En vez de ponerme a explicar línea por línea, te recomiendo que busques el API cada vez que encuentres algo que no entiendes. De forma general, primero he creado un JFrame, no he escogido ningún estilo de diseño predeterminado por Java para poder posicionar cada componente de forma absoluta. El método setIconImage() de JFrame nos permite crear un ícono para la aplicación que no es mostrado en MAC pero en PC sí. La imagen que tenemos como ícono se ve de la siguiente forma en Windows 7: 239 Para mantener la transparencia del fondo, el equipo de diseño nos provee con imágenes .png que permiten transparencias. El método setResizable(false) de JFrame, impide que el usuario le cambie el tamaño a la ventana. Las clases Pintar, TextoAyuda y BotonAyuda son clases internas de Metronomo que extienden JPanel, luego sobrescriben el método paintComponent() y así podemos agregar las tres imágenes: el fondo, el botón de ayuda y la imagen con el texto de ayuda. Esas tres imágenes son las siguientes: 240 Java nos permite posicionar la profundidad de un componente si usamos un JLayeredPane, al cual le vamos a agregar todos los componentes en vez de a JFrame. Cuando agregamos un componente a JLayeredFrame, usamos el método add() que recibe dos parámetros, el primero es el componente a adicionar y el segundo es un objeto de Integer con un número que entre más se acerque a cero más al fondo aparecerá. Cuando hacemos clic sobre el botón de ayuda, el código fondo.setLayer(ayudaText, 2, -1); es el encargado de traer al frente la imagen gracias al número dos, para volverlo a enviar al fondo escribimos el mismo código pero cambiando el 2 por el cero. De resto no hay nada importante para la aplicación que no hayamos visto excepto JSlider y JComboBox que son los encargados del volumen y el menú desplegable respectivamente. Ambos son muy fáciles de usar y para aprender su implementación puedes mirar el código completo en el siguiente capítulo. Luego de la creación del GUI entramos en materia en el método empezar() que contiene el siguiente código: private void empezar() { try { secuenciador = MidiSystem.getSequencer(); secuenciador.open(); Sequence secuencia = new Sequence(Sequence.PPQ, 24); track1 = secuencia.createTrack(); tempo(120); crearSecuencia(Metronomo.NO_DIVISION, 4); secuenciador.setSequence(secuencia); secuenciador.setLoopStartPoint(1); secuenciador.setLoopEndPoint(secuencia.getTickLength() - 1); 241 secuenciador.setLoopCount(Sequencer.LOOP_CONTINUOUSLY); }catch(Exception e){ avisos.setText("Por favor cierra otras aplicaciones Java."); System.out.println(e); } } Aquí creamos un secuenciador por defecto usando MidiSystem.getSequencer(), usamos el método tempo(120) que agrega un meta evento de tempo a la secuencia a una velocidad de 120BPM y creamos por defecto una secuencia con acento cada 4 pulsos sin subdivisión llamando el método crearSecuencia(). Luego usamos los métodos setLoopStartPoint() y setLoopEndPoint() para poder hacer un loop de la secuencia y no tener que crear secuencias que se repitan innecesariamente. Mediante el método setLoopCount() podemos decir cuántas veces queremos que ocurra el loop, en este caso estamos definiedo un loop infinito. En el catch hemos actualizado el texto por si ha ocurrido un error al crear los dispositivos MIDI. La razón más probable para que lleguemos a este punto, es que otras aplicaciones estén utilizando los recursos. Luego tenemos el método crearSecuencia() encargado de hacer ciclos dependiendo de los argumentos: private void crearSecuencia(int tipo, int acento) { acentosCada = acento; division = tipo; boolean suena = secuenciador.isRunning(); secuenciador.stop(); //borra todos los eventos en el track if(track1.size() > 0){ while(track1.size() > 1){ 242 track1.remove(track1.get(1)); } } // crea la secuencia if(acento != 0){ for(int i = 1; i <= acento; i++) { if(i == 1){ track1.add(eventosMIDI(153, 60, 120, i, 0)); track1.add(eventosMIDI(153, 60, 0, i + 1, 0)); }else{ track1.add(eventosMIDI(153, 61, 120, i, 0)); track1.add(eventosMIDI(153, 61, 0, i + 1, 0)); } if(tipo != 0){ track1.add(eventosMIDI(153, 61, 60, i, tipo)); track1.add(eventosMIDI(153, 61, 0, i + 1, 0)); } // para tercera corchea en ternario if(tipo == 15){ track1.add(eventosMIDI(153, 61, 60, i, 7)); track1.add(eventosMIDI(153, 61, 0, i + 1, 0)); } } }else{ track1.add(eventosMIDI(153, 61, 120, 1, 0)); track1.add(eventosMIDI(153, 61, 0, 2, 0)); if(tipo != 0){ track1.add(eventosMIDI(153, 61, 60, 1, tipo)); track1.add(eventosMIDI(153, 61, 0, 2, 0)); 243 } // para tercera corchea en ternario if(tipo == 15){ track1.add(eventosMIDI(153, 61, 60, 1, 7)); track1.add(eventosMIDI(153, 61, 0, 2, 0)); } } secuenciador.setLoopEndPoint(secuenciador.getTickLength() - 1); secuenciador.setTickPosition(1); if(suena){ secuenciador.start(); } } Una de las cuestiones más interesantes de este código es que permite mantener la secuencia sonando y actualizarse en tiempo real, si no estaba sonando también puede actualizarse obviamente. Analiza este código por tu cuenta y verás que no hay nada que no hayamos visto durante este texto. La clave del método crearSecuencia() es la forma en que trabajan sus ciclos dependiendo de los argumentos recibidos. Este método hace uso extensivo del método eventosMIDI() que resulta demasiado útil en este tipo de aplicaciones. static MidiEvent eventosMIDI(int status, int data1, int data2, int pulso, int division) { ShortMessage mensaje = new ShortMessage(); try { mensaje.setMessage(status, data1, data2); }catch(Exception e){ //Por ser static no podemos usar la variable al texto avisos. System.out.println(e); } 244 if(division != Metronomo.NO_DIVISION){ return new MidiEvent(mensaje, ((pulso * 24) - division)); } return new MidiEvent(mensaje, ((pulso * 24) - 23)); } Este método es demasiado simple y al mismo tiempo resulta muy efectivo ya que nos está ahorrando cientos de líneas en esta aplicación. Su función es devolver un MidiEvent de acuerdo con los argumentos recibidos que expliqué al comienzo de este capítulo. Los eventos setTempo(), tapTempo() y tempo() se encuentran relacionados por obvias razones. public void setTempo(int bpm) { if(bpm >= 30 && bpm <= 300) { boolean encendido = secuenciador.isRunning(); secuenciador.stop(); tempo(bpm); if(encendido){ secuenciador.start(); } }else{ avisos.setText("Por favor escribe un bpm entero entre 30 y 300"); } } private void tempo(int bpm) { velocidad = bpm; track1.remove(track1.get(0)); 245 int tempo = 60000000/bpm; byte[] data = new byte[3]; data[0] = (byte)((tempo >> 16) & 0xFF); data[1] = (byte)((tempo >> 8) & 0xFF); data[2] = (byte)(tempo & 0xFF); MetaMessage meta = new MetaMessage(); try { meta.setMessage(81, data, data.length); MidiEvent evento = new MidiEvent(meta, 0); track1.add(evento); }catch(Exception e){ avisos.setText("Problema al ajustar el tempo del metrónomo."); System.out.println(e); } } private void tapTempo(long now) { antes = ahora; ahora = now; long diferencia = ahora - antes; if(diferencia > 0) { double bpm = ((double)60000 / (double)diferencia); if(bpm >= 30 && bpm <= 300) { setTempo((int)bpm); texto.setText("" + (int)(bpm)); } } } 246 De estos tres métodos, el verdadero encargado del cambio de tempo es tempo(). Por orden los he separado, así cada uno tiene una función específica dictando el tempo. setTempo() se asegura de que no pasen bpm de menos de 30 ni de más de 300, olbigándolo a recibir solo enteros. En toda aplicación debemos pensar que el usuario puede escribir una letra en el campo de texto, o tratar de escribir un número decimal con décimas, etc. Siempre es mejor pensar, sin ofender a los usuarios, que ellos van a cometer todas las equivocaciones posibles, y no queremos que nuestra aplicación falle cuando esto ocurra. El código en general es sacado del apartado de MIDI de este proyecto de grado. Me pareció útil crear un método llamado update() encargado de restablecer los valores de los campos de texto cuando un usuario presiona un botón: private void update() { textoAcento.setText("" + acentosCada); texto.setText("" + velocidad); } Cuando un botón se presiona, este método es llamado, devolviendo así los valores de los textos del acento y la velocidad a sus valores reales por si el usuario los ah modificado pero no ha hecho clic en el botón de modificación. De aquí en adelante solo quedan las clases internas que manejan los eventos y las que crean los JPanel. En el siguiente capítulo puedes mirarlos con detenimiento ya que agrego el código completo, pero no me detengo a analizarlos aquí porque son bastante simples y poco tienen que ver con manejo de audio o MIDI, ya que normalmente actualizan algunas variables y llaman los métodos antes vistos. De todas formas mira detenidamente esas clases ya que son la clave para entender los eventos de la aplicación. 247 La única clase interna que de verdad tiene que ver con audio y que no se explicó a fondo en la sección de MIDI, es la clase que maneja el volumen de una secuencia MIDI. Para lograrlo, he usado un Control Change en el canal 10, este es el status byte 185, que en el control 7 modifica el volumen. El problema es que solo agregar el evento trae problemas debido a que el usuario puede cambiar constantemente el volumen, en cuyo caso la secuencia se llenaría de eventos de volumen. La solución es que mediante un ciclo se busca en la secuencia el evento de volumen, se borra y se actualiza, todo esto ocurre tan rápido que es casi imperceptible, aunque para lograrlo debemos parar la secuencia por un tiempo muy corto. Si bien este código genera el metrónomo que el equipo de La.Do.Mi.Cilio estaba buscando, no por eso es un código perfecto. Hay ciertas imperfecciones que pueden mejorarse sin mucho esfuerzo y permitirían que la clase fuera más reutilizable y más enfocada a objetos, porque si lo piensas bien, aunque estamos usando los objetos, aunque estamos usando la herencia y aunque estamos protegiéndonos mediante la encapsulación, la clase Metronomo está lejos de poder ser usado de forma fácil en otras aplicaciones. Se me ocurre poder separar la parte visual del código de la que realmente genera el metrónomo, esto con el fin de poder tener una única clase llamada Metronomo que realmente nos funcione como un objeto. Voy a hacer ciertas modificaciones sobre el código hasta aquí visto, de tal forma que al final podamos usar el siguiente código para hacer sonar un metrónomo: Metronomo metronomo = new Metronomo(); metronomo.start(); También voy a agregar otros métodos como stop(), setTempo(), getTempo(), isRunning() y setVolume() entre otros, que no tendrán código nuevo, simplemente será una forma de ordenar mejor el código que ya tenemos pensando en que algún día puedo necesitar un metrónomo, y por haber creado un verdadero objeto podremos reutilizarlo. 248 Resultado y código completo Luego de varias pruebas, este es el aspecto visual del metrónomo en Windows 7: El código se ha probado de forma extensa para asegurarse que esté libre de errores, su exactitud rítmica en los sistemas probados es totalmente aceptable. Los usuarios no han reportado fallas ni en Windows xp, Vista o 7. Tampoco para Mac OS X Snow Leopard. El peso final del metrónomo es de 282KB lo que lo hace realmente portable y es una excelente herramienta de estudio. Dentro de www.ladomicilio.com, esta herramienta se encuentra como un Applet, que es un programa Java incrustado en una página web. Lograr un Applet requiere otros conocimientos de programación y por ahora te dejo esta posibilidad como una inquietud que puedes ir aprender por tu cuenta. Por ahora con los conocimientos que tienes puedes crear aplicaciones de escritorio entregando archivos JAR que son muy cómodos y portables para los usuarios. 249 Este es el código completo de la primera versión del metrónomo MIDI hecho en Java para La.Do.Mi.Cilio. Observa todos los cambios que se hicieron respecto al código del capítulo pasado. La siguiente es la estructura de archivos en NetBeans: El paquete com.ladomicilio es un paquete en el que guardo todas mis clases personalizadas, de tal forma que en cualquier otra aplicación simplemente importo dicho paquete. En total se usaron dos archivos Java: Main.java con el código del GUI y Metronomo.java con el código del metrónomo. El siguiente es el código de Main.java: package metronomoladomicilio; import com.ladomicilio.Metronomo; import javax.swing.*; import java.awt.event.*; import javax.swing.event.*; import java.awt.*; public class Main { 250 //variables de instancia private JTextField texto; private JLayeredPane fondo; private JButton botonStartStop; private JComboBox comboBox; private JSlider slider; private JLabel avisos; private JTextField textoAcento; private JPanel ayudaText; private boolean enAyuda = false; Metronomo metronomo; public static void main(String[] args) { Main main = new Main(); main.goMain(); } public void goMain(){ try{ metronomo = new Metronomo(); }catch(Exception e){ avisos.setText(e.getMessage()); } gui(); } private void gui() { JFrame marco = new JFrame("Metrónomo"); marco.setLayout(null); marco.setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE); 251 marco.setSize(400, 357); marco.setIconImage(new ImageIcon(getClass().getResource("images/icon.png")).getImage()); marco.setResizable(false); // JPanel para fondo Pintar pintar = new Pintar(); pintar.setBounds(0, 0, 400, 335); fondo = new JLayeredPane(); fondo.setBounds(0, 0, 400, 335); fondo.add(pintar, new Integer(0)); marco.setContentPane(fondo); // campo de texto para escribir el tempo texto = new JTextField("120"); texto.setBounds(40,88,100,20); texto.setHorizontalAlignment(JTextField.CENTER); fondo.add(texto, new Integer(1)); // botón setTempo JButton botonSetTempo = new JButton("Cambiar tempo (bpm)"); botonSetTempo.setBounds(194,87,170,20); fondo.add(botonSetTempo, new Integer(1)); botonSetTempo.addActionListener(new EventoTempo()); botonSetTempo.setCursor(new Cursor(Cursor.HAND_CURSOR)); // botón tapTempo JButton botonTapTempo = new JButton("Tap Tempo"); botonTapTempo.setBounds(210,204,170,20); fondo.add(botonTapTempo, new Integer(1)); botonTapTempo.addActionListener(new EventoTap()); botonTapTempo.setCursor(new Cursor(Cursor.HAND_CURSOR)); // botón start stop 252 botonStartStop = new JButton("Iniciar"); botonStartStop.setBounds(152,29,100,20); fondo.add(botonStartStop, new Integer(1)); botonStartStop.addActionListener(new StartStop()); botonStartStop.setCursor(new Cursor(Cursor.HAND_CURSOR)); // Combo box para escoger la subdivisión String[] lista = {"Sin división", "Straight", "Shuffle", "Ternario"}; comboBox = new JComboBox(lista); comboBox.setBounds(15,199,150,30); fondo.add(comboBox, new Integer(1)); comboBox.addActionListener(new Division()); comboBox.setCursor(new Cursor(Cursor.HAND_CURSOR)); // slider para volumen slider = new JSlider(JSlider.HORIZONTAL, 0, 127, 100); slider.setBounds(195,245,150,60); fondo.add(slider, new Integer(1)); slider.addChangeListener(new Volumen()); slider.setOpaque(false); // campo de texto para avisos de errores y otros avisos = new JLabel("", JLabel.CENTER); avisos.setForeground(Color.white); avisos.setBounds(0,298,400,30); fondo.add(avisos, new Integer(1)); // campo de texto para escribir el tempo textoAcento = new JTextField("4"); textoAcento.setBounds(40,147,100,20); textoAcento.setHorizontalAlignment(JTextField.CENTER); fondo.add(textoAcento, new Integer(1)); // botón setTempo 253 JButton botonSetAcento = new JButton("Cambiar acento"); botonSetAcento.setBounds(194,145,170,20); fondo.add(botonSetAcento, new Integer(1)); botonSetAcento.addActionListener(new EventoAcento()); botonSetAcento.setCursor(new Cursor(Cursor.HAND_CURSOR)); // Botón ayuda JButton ayuda = new JButton(new ImageIcon(getClass().getResource("images/help.png"))); ayuda.setBounds(358,6,30,30); fondo.add(ayuda, new Integer(2)); ayuda.addActionListener(new Ayuda()); ayuda.setCursor(new Cursor(Cursor.HAND_CURSOR)); ayuda.setBackground(Color.BLACK); // Panel de ayuda ayudaText = new TextoAyuda(); ayudaText.setForeground(Color.white); ayudaText.setBounds(0,0,400,335); ayudaText.setBackground(Color.black); fondo.add(ayudaText, new Integer(2)); fondo.setLayer(ayudaText, 0, -1); // La siguiente línea debe ir de último en todo GUI marco.setVisible(true); } private void update() { textoAcento.setText("" + metronomo.getAcentosCada()); texto.setText("" + metronomo.getTempo()); } // clases internas para eventos: class EventoTempo implements ActionListener { 254 public void actionPerformed(ActionEvent event) { avisos.setText(""); try { metronomo.setTempo(Integer.parseInt(texto.getText())); update(); }catch(Exception e){ System.out.println(e); avisos.setText("Por favor escribe un bpm entero entre 30 y 300."); } } } private class EventoTap implements ActionListener { public void actionPerformed(ActionEvent event) { avisos.setText(""); metronomo.tapTempo(); update(); } } private class StartStop implements ActionListener { public void actionPerformed(ActionEvent event) { avisos.setText(""); if(metronomo.isRunning()) { metronomo.stop(); botonStartStop.setText("Iniciar"); } else { metronomo.start(); botonStartStop.setText("Parar"); } update(); 255 } } private class Division implements ActionListener { public void actionPerformed(ActionEvent event) { avisos.setText(""); JComboBox cb = (JComboBox)event.getSource(); int division = cb.getSelectedIndex(); if(division == 0){ metronomo.crearSecuencia(Metronomo.NO_DIVISION, metronomo.getAcentosCada()); }else if(division == 1){ metronomo.crearSecuencia(Metronomo.STRAIGHT, metronomo.getAcentosCada()); }else if(division == 2){ metronomo.crearSecuencia(Metronomo.SHUFFLE, metronomo.getAcentosCada()); }else if(division == 3){ metronomo.crearSecuencia(Metronomo.TERNARIO, metronomo.getAcentosCada()); } update(); } } private class Volumen implements ChangeListener { public void stateChanged(ChangeEvent event) { avisos.setText(""); JSlider control = (JSlider)event.getSource(); if (!control.getValueIsAdjusting()) { try{ 256 metronomo.setVolume(control.getValue()); }catch(Exception e){ // el único posible error no puede darse // por eso no hacemos nada al respecto } } update(); } } private class EventoAcento implements ActionListener { public void actionPerformed(ActionEvent event) { avisos.setText(""); try{ metronomo.setAcento(Integer.parseInt(textoAcento.getText())); }catch(Exception e){ System.out.println(e); avisos.setText("Por favor escribe un acento entre 0 y 100."); } update(); } } private class Ayuda implements ActionListener { public void actionPerformed(ActionEvent event) { avisos.setText(""); if(enAyuda){ fondo.setLayer(ayudaText, 0, -1); enAyuda = false; }else{ fondo.setLayer(ayudaText, 2, -1); 257 enAyuda = true; } } } // Clases para GUI private class Pintar extends JPanel { public void paintComponent(Graphics g) { Image imagen = new ImageIcon(getClass().getResource("images/fondo.jpg")).getImage(); g.drawImage(imagen,0,0,this); } } private class BotonAyuda extends JPanel { public void paintComponent(Graphics g) { Image imagen = new ImageIcon(getClass().getResource("images/help.png")).getImage(); g.drawImage(imagen,0,0,this); } } private class TextoAyuda extends JPanel { public void paintComponent(Graphics g) { Image imagen = new ImageIcon(getClass().getResource("images/ayuda.jpg")).getImage(); g.drawImage(imagen,0,0,this); } } } 258 También he agregado excepciones a algunos métodos para asegurar en el futuro un correcto uso de cada método. La siguiente lista muestra los métodos públicos que podemos usar sobre las instancias de Metronomo: crearSecuencia(int tipoDeDivision, int acentoCada): Este método nos crea una nueva secuencia en el canal 10 del metrónomo con las características de los argumentos. Además borra toda secuencia que estaba antes presente. setTempo(int bpm): Nos permite modificar el tempo bpm de nuestro metrónomo. tapTempo(): Es un método que al llamarlo repetidas veces genera un tempo de acuerdo con el intervalo de tiempo entre cada llamado. getTempo(): Devuelve un entero que representa el tempo actual en bpm. getAcentosCada(): Devuelve un entero que determina cada cuánto hay un acento fuerte. start(): Hace sonar el metrónomo. stop(): Detiene el metrónomo. isRunning(): Devuelve un booleano que determina si está sonando o no el metrónomo. setVolume(int data): Permite seleccionar el volumen, debe ser un valor entre 0 y 127. setAcento(int acento): Selecciona cada cuántos pulsos queremos un acento fuerte. El siguiente es el código de Metronomo.java: 259 package com.ladomicilio; import javax.sound.midi.*; public class Metronomo { // constantes public final static int NO_DIVISION = 0; public final static int STRAIGHT = 11; public final static int SHUFFLE = 7; public final static int TERNARIO = 15; // private Sequencer secuenciador; private Track track1; private long antes; private long ahora = 1; private int division; private int acentosCada; private int velocidad; private boolean running = false; public Metronomo() throws ExcepcionPrincipal{ try { secuenciador = MidiSystem.getSequencer(); secuenciador.open(); Sequence secuencia = new Sequence(Sequence.PPQ, 24); track1 = secuencia.createTrack(); tempo(120); crearSecuencia(Metronomo.NO_DIVISION, 4); 260 secuenciador.setSequence(secuencia); secuenciador.setLoopStartPoint(1); secuenciador.setLoopEndPoint(secuencia.getTickLength() - 1); secuenciador.setLoopCount(Sequencer.LOOP_CONTINUOUSLY); }catch(Exception e){ System.out.println(e); throw new ExcepcionPrincipal(); } } public void crearSecuencia(int tipo, int acento) { acentosCada = acento; division = tipo; boolean suena = secuenciador.isRunning(); secuenciador.stop(); //borra todos los eventos en el track if(track1.size() > 0){ while(track1.size() > 1){ track1.remove(track1.get(1)); } } // crea la secuencia if(acento != 0){ for(int i = 1; i <= acento; i++) { if(i == 1){ track1.add(eventosMIDI(153, 60, 120, i, 0)); track1.add(eventosMIDI(153, 60, 0, i + 1, 0)); }else{ track1.add(eventosMIDI(153, 61, 120, i, 0)); 261 track1.add(eventosMIDI(153, 61, 0, i + 1, 0)); } if(tipo != 0){ track1.add(eventosMIDI(153, 61, 60, i, tipo)); track1.add(eventosMIDI(153, 61, 0, i + 1, 0)); } // para tercera corchea en ternario if(tipo == 15){ track1.add(eventosMIDI(153, 61, 60, i, 7)); track1.add(eventosMIDI(153, 61, 0, i + 1, 0)); } } }else{ track1.add(eventosMIDI(153, 61, 120, 1, 0)); track1.add(eventosMIDI(153, 61, 0, 2, 0)); if(tipo != 0){ track1.add(eventosMIDI(153, 61, 60, 1, tipo)); track1.add(eventosMIDI(153, 61, 0, 2, 0)); } // para tercera corchea en ternario if(tipo == 15){ track1.add(eventosMIDI(153, 61, 60, 1, 7)); track1.add(eventosMIDI(153, 61, 0, 2, 0)); } } secuenciador.setLoopEndPoint(secuenciador.getTickLength() - 1); secuenciador.setTickPosition(1); if(suena){ secuenciador.start(); 262 } } private static MidiEvent eventosMIDI(int status, int data1, int data2, int pulso, int division) { ShortMessage mensaje = new ShortMessage(); try { mensaje.setMessage(status, data1, data2); }catch(Exception e){ //Por ser static no podemos usar la variable al texto avisos. System.out.println(e); } if(division != Metronomo.NO_DIVISION){ return new MidiEvent(mensaje, ((pulso * 24) - division)); } return new MidiEvent(mensaje, ((pulso * 24) - 23)); } public void setTempo(int bpm) throws BPMIncorrecto, ExcepcionTempo{ if(bpm >= 30 && bpm <= 300) { boolean encendido = secuenciador.isRunning(); secuenciador.stop(); try{ tempo(bpm); }catch(Exception e){ throw new ExcepcionTempo(); } if(encendido){ secuenciador.start(); } }else{ 263 throw new BPMIncorrecto(); } } private void tempo(int bpm) throws ExcepcionTempo{ velocidad = bpm; track1.remove(track1.get(0)); int tempo = 60000000/bpm; byte[] data = new byte[3]; data[0] = (byte)((tempo >> 16) & 0xFF); data[1] = (byte)((tempo >> 8) & 0xFF); data[2] = (byte)(tempo & 0xFF); MetaMessage meta = new MetaMessage(); try { meta.setMessage(81, data, data.length); MidiEvent evento = new MidiEvent(meta, 0); track1.add(evento); }catch(Exception e){ System.out.println(e); throw new ExcepcionTempo(); } } public void tapTempo(){ antes = ahora; ahora = System.currentTimeMillis(); long diferencia = ahora - antes; if(diferencia > 0) { double bpm = ((double)60000 / (double)diferencia); if(bpm >= 30 && bpm <= 300) { try{ 264 setTempo((int)bpm); }catch(Exception e){ // no necesitamos hacer nada } } } } public int getTempo(){ return velocidad; } public int getAcentosCada(){ return acentosCada; } public void start() { if(!secuenciador.isRunning()) { secuenciador.start(); running = true; } } public void stop() { if(secuenciador.isRunning()) { secuenciador.stop(); running = false; } } public boolean isRunning(){ return running; } public void setVolume(int volumen) throws ExcepcionVolumen{ 265 boolean suena = secuenciador.isRunning(); secuenciador.stop(); // Busca un evento de volumen y lo borra for(int i = 0; i < track1.size(); i++){ if(track1.get(i).getMessage().getStatus() == 185){ track1.remove(track1.get(i)); } } // Cambia el volumen secuenciador.getTickPosition(); ShortMessage mensaje = new ShortMessage(); try { mensaje.setMessage(185, 7, volumen); }catch(Exception e){ System.out.println(e); throw new ExcepcionVolumen(); } track1.add(new MidiEvent(mensaje, secuenciador.getTickPosition() + 5)); if(suena){ secuenciador.start(); } } public void setAcento(int a) throws ExcepcionAcento{ if( a >= 0 && a <= 100){ crearSecuencia(division, a); }else{ throw new ExcepcionAcento(); } } 266 // Excepciones class ExcepcionPrincipal extends Exception{ public ExcepcionPrincipal(){ super("Por favor cierra otras aplicaciones Java."); } } class BPMIncorrecto extends Exception{ public BPMIncorrecto(){ super("Por favor escribe un bpm entero entre 30 y 300."); } } class ExcepcionTempo extends Exception{ public ExcepcionTempo(){ super("Problema al ajustar el tempo del metrónomo."); } } class ExcepcionVolumen extends Exception{ public ExcepcionVolumen(){ super("Problema con el mensaje de volumen."); } } class ExcepcionAcento extends Exception{ public ExcepcionAcento(){ super("Por favor escribe un acento entre 0 y 100."); } } } 267 Conclusiones 1. Mucho más allá del tema que presento en este proyecto de grado, creo que es necesario concientizar sobre una gran necesidad local y global por descubrir nuevos mercados para el ingeniero de sonido y por qué no para el músico también. No podemos seguir dependiendo de las ya bastante agotadas formas de trabajo típicas a las que estamos acostumbrados. El camino para encontrar diferentes trabajos asociados a nuestra práctica, siempre es un camino que requiere aprendizaje de otras disciplinas y puede no ser fácil. Sin embargo, es hora de ampliar el horizonte e ir en busca de mercados poco explotados que muevan el mundo. Si bien hoy día la piratería le hace mucho daño a la industria musical, no podemos quedarnos quejando sobre la situación, tampoco podemos abandonar nuestra práctica como ingenieros de sonido ya que si decidimos estudiarla es porque tenemos un gusto y una inclinación hacia ella. Este escrito es un ejemplo en la búsqueda por nuevos mercados y nuevos lugares de trabajo, si bien el camino de aprendizaje es largo y no es fácil, para las personas que lo puedan encontrar apasionante, puede ser la clave para sobrevivir en una rama poco explorada de nuestra profesión. Si bien seguir este camino no necesariamente significa el éxito, para mí ha significado parte de un proceso fundamental en mi vida que me ha permitido demostrarme a mí mismo que es posible vivir bien manteniéndome en el mundo del audio. 2. Java sobresale entre muchos otros lenguajes por su portabilidad. Es muy cómodo terminar un código, compilar y crear un único archivo JAR que puede ser llevado de una plataforma a otra. Aunque no enseñé la forma en que se pueden crear Applets10 debido a que esto requiere el aprendizaje de la clase Applet y cierto conocimiento sobre XHTML que se salen de los límites de este trabajo, si debo decir que gracias a los Applets, podemos tener aplicaciones de audio demandantes que de ningún otro modo son posibles dentro de una página web. 10 Un Applet es una aplicación Java incrustada en una página web. Esto permite que el usuario no tenga que descargar dicha aplicación para usarla, simplemente accede a la página y eso es todo. 268 3. Java es un lenguaje interpretado, esto le permite su portabilidad y estabilidad a través de diferentes plataformas. Podemos pensar que un lenguaje como Java está en otro idioma que el que manejan los computadores, debido a esto necesita un traductor que es el JVM o Java Virtual Machine. Los lenguajes interpretados tienden a ser más lentos precisamente porque deben ser traducidos. Sin embargo, el resultado final después de compilar un archivo Java es el bytecode, que es un código muy cercano al lenguaje de las máquinas y gracias a esto es muy rápido a pesar de ser interpretado. 4. En el capítulo 'Capturar grabar y reproducir', recomendé hacer una aplicación que capturara el micrófono e inmediatamente saliera el sonido en tiempo real por los parlantes. Aunque podemos mover el tamaño del buffer del TargetDataLine, el del SourceDataLIne, e incluso podemos cambiar el tamaño del arreglo de bytes, he probado la aplicación en diferentes entornos, en muy buenos computadores y aunque la latencia puede llegar a ser baja, siempre es perceptible. Esto nos impide crear aplicaciones de audio en tiempo real. La buena noticia es que todos los días los computadores van mejorando, y con cada actualización Java se hace más rápido, si bien hoy día no es una buena idea usar Java para crear aplicaciones de audio que necesiten una latencia lo más cercana a cero, no es raro que en pocos años esto se vuelva posible. Esta latencia existe debido al tema mencionado en la conclusión anterior, si bien Java es muy rápido para ser interpretado, de por sí las aplicaciones en tiempo real son bastante demandantes y Java le agrega una capa de latencia a dicho proceso. 5. El API de MIDI en Java es muy poderoso, nos permite trabajar al nivel de los bytes y además es robusto y flexible. Esto nos permite crear infinidad de aplicaciones. Aunque en este texto no pude abarcar todo el contenido del API, pudimos ver que gracias a Java podemos controlar aparatos externos vía MIDI, recibir información desde el exterior y crear aplicaciones que no sólo se comuniquen con el entorno sino también puedan ser por sí solas muy útiles como 269 el metrónomo de La.Do.Mi.Cilio. Además la precisión rítmica es bastante buena. Una vez entendidas las bases de cómo funciona el MIDI, es fácil trabajar con el API y cuando se exploran más a fondo sus clases, interfaces y métodos, se descubre todo un mundo de posibilidades. Llego a pensar que hacer un programa como Reason o Finale es posible usando Java. 6. El API de sonido sampled tiene a su favor que es de bajo nivel y nos permite llegar a manipular incluso los puertos de los aparatos físicos instalados en el sistema. Aunque no los agregué en este trabajo de grado por ser temas avanzados para un primer acercamiento a Java, este API es lo suficientemente poderoso para poder crear sonido sintético, manipular y analizar los bits entrantes y salientes de audio. Sin embargo su estructura e implementación no es nada agradable ni fácil de entender y creo que el peor error que tiene es usar términos como Mixer en aparatos que nada tienen que ver con una consola, esto dificulta su implementación. Otra terminología como 'targets' y 'sources' terminan de complicar la situación porque las líneas deben pensarse según el Mixer y no según la aplicación. Por ejemplo necesitamos un 'target' para capturar la información proveniente del micrófono, esto no tiene mucho sentido en cuanto que desde el punto de la aplicación un micrófono no puede ser un destino sino una fuente. Para terminar de complicar la situación los puertos si son nombrados correctamente, las entradas son 'source' y las salidas son 'target'. Por otra parte los puertos pueden abrirse y cerrarse permitiendo así el flujo de información, pero no podemos acceder directamente a la información proveniente de ellos. En definitiva el API de audio en sí también es muy poderoso y robusto, pero trabajar con él no es tan sencillo en comparación con el de MIDI. 6. Si bien Java nos permite crear nuestros propios API capaces de leer mp3 y otros formatos, incluso creados por nosotros mismos, los formatos que permite por defecto son pocos y sería increíble que fuera un poco más abierto en este sentido. El mp3 o el AAC son ampliamente usados y aunque deterioren la calidad, son una excelente opción para ciertas aplicaciones ya que su peso es bastante bajo. Si por 270 ejemplo queremos hacer un reproductor de audio para el computador, el usuario no podrá reproducir mp3, lo que va a encontrar frustrante. Para solucionarlo podemos buscar varios API que encontramos en la web que nos permiten reproducirlo, pero sería mucho más cómodo que Java manejara este tipo de archivos. 7. La información sobre los API de sonido y MIDI es muy limitada. Cuando se encuentra algo, las descripciones son pocas y no es fácil de entender. Incluso la misma documentación que brinda Java es confusa, enredada y le falta explicación con ejemplos más claros. Esto sumado a que no son APIs nada fáciles comparados por ejemplo con swing para la parte visual que es extremadamente sencilla. Se hace necesario para el mundo del audio que se creen muchos más documentos formales con mayor investigación sobre la parte de audio en Java. Esto ayudaría a Sun, creadores de Java, a ponerle más cuidado a este tema. 8. Tener claras las bases del lenguaje nos permitirá trabajar de forma más robusta y ágil con la parte de audio. Es muy agradable saber que aunque nos faltó mucho por aprender del lenguaje, con estos conocimientos es suficiente para empezar a crear aplicaciones muy poderosas. Si bien el aprendizaje de un nuevo lenguaje no es tan sencillo, con cada nuevo código que creamos estamos aprendiendo y asimilando la forma en que se debe trabajar. Definitivamente leer un libro sobre Java no es suficiente, es programando, cometiendo errores, teniendo paciencia y sobre todo sabiendo resolver problemas que podemos terminar con la aplicación que nos hemos propuesto en un comienzo. 9. Java es un lenguaje orientado a objetos y esto lo hace muy poderoso, reutilizable, sostenible y fácil de entender en el futuro. El conocimiento de las reglas que gobiernan el mundo de la programación orientada a objetos nos permite trabajar mejor cuando estamos creando aplicaciones en un equipo de trabajo, pero incluso para nosotros mismos es una ayuda enorme hacia el futuro ya que nos evita reinventar la rueda. Entender el mundo de los objetos no sólo es 271 útil programando sino es básico para poder ver un API y entender cómo se usa. En los API de audio y MIDI tenemos que usar y entender la herencia, el polimorfismo y la encapsulación a plenitud para usar de forma correcta todas sus clases. 10. La creación de un metrónomo para una aplicación de la vida real, es un buen ejemplo de cómo debemos pensar en la creación de un código que nos sirva en el futuro. Más adelante podríamos decidir crear un secuenciador que tenga un metrónomo integrado, lo más agradable de todo es que no tenemos que crearlo porque ya tenemos una clase llamada Metronomo que gracias a los conocimientos de objetos podremos reusar. Si bien hay muchas formas en que se puede mejorar y completar la clase Metronomo, es muy agradable saber que desde otra aplicación podemos usar el siguiente código y tendremos un metrónomo sonando: Metronomo metro = new Metronomo(); metro.start(); Es entonces en la creación de una aplicación real que entendemos la importancia de tener claras las bases y la teoría de Java, el audio y el MIDI. 272 Bibliografía 1. 2010 "Java SE 6 API Documentation". Oracle Corporation y sus afiliados. < http://download.oracle.com/javase/6/docs/api/>. [Consulta: Octubre 24 de 2010] 2. 2010. "Java Sound API". Oracle Corporation y sus afiliados. <http://java.sun.com/products/java-media/sound/ >. [Consulta: Noviembre 1 de 2010]. 3. 2010. "Rich Internet Application Statistics". Desarrollado por DreamingWell.com. < http://riastats.com/> [Consulta: 15 de Agosto de 2010]. 4. 2010. "The Java Tutorials". Oracle Corporation y sus afiliados. <http://download.oracle.com/javase/tutorial/sound/sampled-overview.html>. [Consulta: Noviembre 9 de 2010]. 5. Baldwin, Richard. 2003. "Advanced Java Programming Tutorial: Java Sound, An Introduction". <http://www.dickbaldwin.com/tocadv.htm> [Consulta: 1 de Septiembre de 2010]. 6. Bates, Bert y Sierra, Kathy. 2005. Head First Java, Segunda edición. Estados Unidos. O' Reilly Media, Inc. 7. Bomers, Florian y Pfisterer, Matthias. 2005. "Java Sound Resources". <http://www.jsresources.org/index.html >. [Consulta: 10 de Septiembre de 2010] 8. Rona, Jeffrey. 1994. The MIDI companion. Estados Unidos. Hal Leonard Corporation. 9. Schildt, Herbert. 2009. JAVA Manual de referencia, Séptima edición. México, D.F: McGraw-Hill. 273