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