programación en java desde la perspectiva del audio

278
Programación en Java desde la perspectiva del audio

Upload: others

Post on 04-Oct-2021

2 views

Category:

Documents


0 download

TRANSCRIPT

Programación en Java desde la perspectiva del audio

2

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

i

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

ii

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

iii

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

1

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.

2

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

3

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

4

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.

5

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/

6

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.

7

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

8

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

9

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.

10

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.

11

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.

12

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.

13

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

14

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

15

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.

16

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í.

17

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.

18

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.

19

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.

20

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:

21

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.

22

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.

23

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.

24

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");

}

}

25

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 class ya sea

MiPrimerPrograma, MiSegundoPrograma o NombreDeLaClase. 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

26

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,

27

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.

28

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:

29

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.

30

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

31

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

32

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

33

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

34

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.

35

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

36

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.

37

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

38

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

39

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

40

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

41

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.

42

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

43

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.

44

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.

45

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);

}

}

46

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

47

(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 ++;

48

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

49

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.

50

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.

51

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.");

}

}

}

52

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"):

53

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:

54

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:

55

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í.

56

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 }

57

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

58

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.

59

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.

60

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

61

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.

62

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.

63

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");

}

}

64

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

65

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.

66

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] + " ");

}

}

}

67

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

68

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

69

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();

70

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.

71

Á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

72

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.

73

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);

74

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

75

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

76

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;

77

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().

78

¿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.

79

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.

80

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.

81

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

82

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

83

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.

84

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

85

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

86

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,

87

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);

88

}

}

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

89

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;

90

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

91

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

92

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().

93

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

94

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

95

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

96

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

97

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

98

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.

99

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

100

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.

101

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í:

102

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]);

}

103

}

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());

104

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);

105

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:

106

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.

107

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:

108

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

109

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

110

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.

111

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

112

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

113

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

114

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");

}

}

115

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.

116

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

117

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

118

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.

119

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() {

120

}

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

121

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.

122

¿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

123

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.

124

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

125

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

126

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.

127

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);

128

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. En la segunda línea usamos el método

setDefaultCloseOperation(), al cual le pasamos la siguiente 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

129

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

130

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

131

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:

132

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);

}

}

133

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");

134

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:

135

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:

136

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");

137

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.

138

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:

139

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

140

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);

141

}

}

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

142

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.

143

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:

144

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

145

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.

146

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

147

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("");

}

}

}

148

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

149

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:

150

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());

}

}

}

151

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.

152

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

153

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

154

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

155

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

156

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.

157

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.

158

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

159

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

160

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

161

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.

162

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

163

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

164

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.

165

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,

166

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 try-

catch. 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

167

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);

}

}

}

168

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

169

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.

170

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:

171

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

172

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 NOTE-

ON 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 NOTE-

OFF 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

173

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.

174

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

175

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();

176

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:

177

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:

178

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.

179

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

180

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 NOTE-

ON 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:

181

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);

}

182

}

}

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.

183

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.

184

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

185

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 data-

bytes 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

186

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

187

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) + ", ");

188

}

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.

189

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 meta-

eventos, necesitamos usar el objeto llamado MetaMessage. El siguiente código,

190

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 meta-

evento 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

191

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"));

192

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();

193

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

194

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.

195

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

196

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.

197

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

198

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

199

ú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:

200

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:

201

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

202

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.

203

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

204

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.

205

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í solo9, 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.

206

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 low-

level 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.

207

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.*;

208

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

209

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

210

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.

211

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

212

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

213

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 A-

LAW 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:

214

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

215

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:

216

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]);

217

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:

218

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.

219

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.

220

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)

221

{

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() {

222

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.

223

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();

224

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

225

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.

226

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

227

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.

228

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

229

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á

230

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:

231

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:

232

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();

233

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.

234

- 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

235

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;

236

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);

237

// 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());

238

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());

239

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:

240

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:

241

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);

242

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){

243

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));

244

}

// 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);

}

245

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));

246

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));

}

}

}

247

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.

248

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.

249

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.

250

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 {

251

//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);

252

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

253

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

254

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 {

255

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();

256

}

}

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{

257

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);

258

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);

}

}

}

259

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:

260

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);

261

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));

262

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();

263

}

}

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{

264

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{

265

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{

266

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();

}

}

267

// 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.");

}

}

}

268

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.

269

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

270

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

271

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

272

ú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.

273

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.