Como reproducir una grabación en iOS 9.0 usando Swift 2.0

Posted by in Apple, iPhone, Programación, Tecnología

Introducción

En este artículo voy a tratar de dar lo que creo es un buen ejemplo de como grabar, guardar y reproducir un audio usando Xcode 7, Swift 2.0 en iOS 9.

Escribo este artículo porque estoy llevando el programa de Udacity: iOS Development Nanodegree para actualizarme con Swift 2.0 y Xcode 7 lo más rápido posible (aun no existe mucha literatura sobre la nueva versión de Swift) y una de las tareas que nos han encargado como parte del primer curso es escribir un artículo que nos permita demostrar algunos de los temas vistos hasta ahora.

Como ya tengo “algo” de experiencia programando, este artículo va un poco más allá de lo que hemos cubierto en el curso por lo que si están en el mismo programa que yo y no entienden partes del código, no se preocupen lo vamos a cubrir en el curso (sí, ya revisé 😉 ). Mientras tanto pueden dejarme un comentario en este artículo o dejar la pregunta en el foro de Udacity, lo visito seguido.

Los objetivos

Antes de iniciar cualquier proyecto, sea o no de desarrollo lo primero que hay que hacer es acotar los alcances del mismo, es muy fácil pasar de algo muy simple a un pequeño monstruo de varias cabezas si no los tenemos claro. Si se les ocurren una de esas ideas a lo largo del camino como “que tal si agregamos esto” o “Porque no modificamos aquello”, anótenla en un papel y guárdenlo en un cajón y no vuelvan a abrirlo hasta tener lista la primera versión de su app. En serio, si no nunca van a terminar.

Así que tomando en cuenta esto, lo que queremos lograr con esta versión de la app es lo siguiente.

  • Grabar audio a través del micrófono del dispositivo o de los auriculares.
  • Almacenar el audio grabado en una archivo en el dispositivo para poder reproducirlo de forma posterior.
  • Reproducir el audio a través de los parlantes del dispositivo o de los auriculares.
  • Detener, pausar y resumir la reproducción.

Claro está se espera que el código sea claro, entendible y ordenado. Al final de este artículo dejo algunas de esas notas que fueron para el cajón.

Algunas pautas

  • Voy a asumir que ya tienen nociones de programación, que conocen las bases del lenguaje Swift como para reconocer la estructura básica del código que presente, y saben como usar Xcode. No es objetivo de este articulo enseñar Swift, su razón principal es demostrar el uso particular del lenguaje en un escenario particular.
  • Aunque blogueo, hablo y pienso en español, siempre programo en inglés. Uno nunca sabe quién más va a tener que revisar y manipular su código, sin importar la ubicación geográfica. Por lo tanto el nombre de las variables, los comentarios y los textos se desarrollarán en inglés. En un siguiente artículo mostraré como internacionalizar la app para que muestre el contenido en diferentes idiomas.
  • Este artículo y el código que contiene no sería posible si no hubiera leído el libro iOS 8 Development Essentials de Neil Smyth o sin los aportes de muchos otros programadores por allí en la Internet. Trataré de mencionar a todos los que me ayudaron a escribir este artículo sin saberlo.

Empecemos entonces.

AVFoundation, AVAudioRecorder, AVAudioPlayer, AVAudioSession y sus delegados

Lo primero que necesitamos es importar AVFoundation para tener disponibles en nuestro proyectos todas las clases de manipulación de medios. En nuestro caso sólo vamos a utilizar las que están destinadas a la manipulación de audio.

Las clases que vamos a utilizar en este paso a paso son:

  • AVAudioRecorder: necesaria para grabar audio.
  • AVAudioPlayer: necesaria para reproducir audio.
  • AVAudioSession: necesaria para establecer la naturaleza del contexto en que vamos a manipular el audio e indicarle al sistema operativo que tenemos esas intenciones.

Delegados

Para poder asegurarnos que estas clases, específicamente, AVAudioRecorder y AVAudioPlayer nos puedan notificar cuando ciertos eventos ocurran tenemos que implementar los protocolos que ellos esperan. Para esto sólo tenemos que agregar las referencia a los delegados respectivos en la declaración de la clase, como se muestra en la última línea del código anterior.

Outlet y otras variables

En la interface de usuario de mi aplicación en utilizado 4 botones, recordButton (grabar), playButton (reproducir), stopButton (detener la grabación y detener la reproducción) y pauseButton (pausar la reproducción). Coloquen los botones en el storyboard de la aplicación y creen outlets similares a los que mostramos a continuación.

También utilizamos una etiqueta para indicar el estado de nuestra aplicación: “recording” (grabando), “playing” (reproduciendo) o “paused” (En pausa).

Adicionalmente a los outlets necesitamos las siguientes variables, 2 para mantener referencias a las instancias de AVAudioRecorder y AVAudioPlayer, y una tercera para monitorear el resultado de todas las verificaciones que realizamos.

Notar que las variables audioPlayer y audioRecorder son de tipo Optional (pueden leer sobre el tipo de dato Optional en la documentación de Apple: Types y Optional Chaning). Esto es necesario porque al momento de declararlas aun no tenemos acceso a las instancias a las cuales deben de hacer referencia y, además, veremos que es posible que nunca obtengamos estas referencias.

Inicializando nuestra app en viewDidLoad()

Obtener la ruta para nuestro almacenar el audio

En el evento viewDidLoad() lo primero que necesitamos obtener es la ruta del archivo en el directorio por defecto de nuestra aplicación para almacenar el audio grabado, y poder reproducirlo posteriormente.

Nota: es importante resaltar que la mayoría de ejemplos en Internet muestran el uso de la función .stringByAppendingString(“sound.caf”) para construir la ruta pero esta función ya no existe en la versión 2.0 de Swift. Gracias a Dániel Nagy por su colaboración en Stack Overflow es que pude encontrar la manera correcta de hacerlo en Swift 2.0. Gracias Dániel!

A continuación, siempre dentro del método viewDidLoad() debemos configurar las características con las cuales queremos que se manipule el audio.

Como pueden apreciar aquí es Sonido 101, definimos la calidad del audio, la frecuencia del codificador, los canales y la frecuencia de grabación. Como decimos por aquí: “Chancay de a 20” (Algo totalmente sencillo de hacer).

Creando la instancia de AVAudioSession

Esto ya no es de a 20 ni mucho menos un chancay así que enfóquense.  Primero la parte fácil, creamos una instancia de AVAudioSession llamando a la función de clase .sharedInstance().

Solicitando permisos

Para poder tener acceso al micrófono que ofrece el iPhone debemos solicitar los permisos respectivos. Esto para evitar que podamos ponernos a grabar conversaciones sin que el dueño del dispositivo de permiso para ello.

El problema que la acción de solicitar este permiso es ejecutado directamente por el sistema operativo en otro hilo de ejecución, y tenemos que darle los medios al sistema operativo para que sepa que hacer si es que nos dan la autorización y que hacer cuando no y aquí es donde entra la siguiente instrucción. Si revisan el código, está conformada por todo el resto de lineas de código mostradas.

Lo que estamos haciendo es llamar a la función .requestRecordPermission y le pasamos como parámetro un método completo con parámetros y código que ejecutar.

En nuestro caso estas pasando un método que recibe un sólo parámetro granted de tipo Bool, el cual será true (verdadero) si el usuario otorga permisos y false (falso) si no. Este método no retorna valores como lo muestra la porción -> Void.

Este método primero valida si nos han otorgado los permisos, si es así definimos la categoría para la sesión. En este caso debemos utilizar la versión de la llamada que hace referencia a el parámetro withOptions: y especificar la opción .AVAUdioSessionCategoryOptions.DefaultToSpeaker. Esto es importante porque sin esta opción, cuando reproduzcamos el audio este no va a reproducirse por el parlante principal del iPhone, si no que se ejecutará por el parlante destinado a ser usando cuando apoyamos la oreja en el dispositivo.

Las primera vez que jugué con audio en el iPhone me rompí la cabeza por horas porque no escuchaba nada y estaba convencido que era un error / bicho / bug de swift. Hasta que por casualidad me di cuenta que si salía sonido, pero extremadamente bajo … ajá el sonido era reproducido por el parlante pequeño. Ahora si tenía una pista para buscar como arreglar este comportamiento .. a guguelear el problema. De nuevo me salvó StackOverflow, esta vez gracias a Nick que propuso una respuesta al problema.

Después creamos la instancia de AVAudioRecorder pasando el URL del archivo a utilizar soundFileURL y las parámetros para el grabado recordSettings. Le indicamos a la instancia que se prepare para grabar llamando a su método .prepareToRecord() y le asignamos la clase ViewControler como delegado.

do … try … catch

Notar que en todas estas llamadas utilizamos la estructura do … try … catch para realizar las llamadas. Esta es la nueva forma de capturar errores en Swift 2.0. Les recomiendo leer este artículo sobre el tema: http://sketchytech.blogspot.pe/2015/06/swift-20-day-one-do-try-catch.html o también la referencia en el sitio de Apple Developer.

Lo que realizamos aquí es validar que obtenemos los permisos y además somos capaces de crear todas las instancias necesarias. Si algunas de estas acciones no es posible le asignamos el valor de false (falso) a la variable areWeOK. Esta variable la utilizaremos más adelante para saber si podemos habilitar los botones de la interface.

Un Metodo de Aperitivo

Antes de continuar definamos un método para controlar el estado de los diversos botones de la interface de nuestra aplicación. Se explica sólo así que no entraré en detalles. Sólo una observación, como verán aquí es donde consultamos la variable areWeOK, y solo si su valor es true (verdadero) procedemos a establecer los estados a los botones. Caso contrario deshabitamos todos los botones de la interface.

Documentar nuestro código con comentarios

comments-tagsSi observan detenidamente el código anterior pueden notar que he utilizado algunas etiquetas en los comentarios y un forma peculiar de formatear los comentarios de este método en particular. Las etiquetas tienen una función particular que nos permiten definir mensaje en la IDE de Xcode como se ve en la siguiente imagen.

Las etiquetas reconocidas por Xcode son:

  • // MARK: Usada para agrupar funciones relacionadas. Si solo se coloca un guión después de la etiqueta Xcode intentará una linea.
  • // TODO: Usada para indicar una tarea pendiente.
  • // FIXME: Usada para indicar un segmento de código o error que necesita ser corregido.

Para mayor información sobre el uso de las etiquetas en los comentarios pueden revisar este artículo: Organizing Your Swift Code en el sitio web Learn Swift Online.

comments-documentationY la forma peculiar de los comentarios le permite a Xcode “entenderlo” y usarlo para automatizar parte de la documentación de nuestra aplicación como se muestra en la siguiente imagen.

Pueden obtener más información sobre las etiquetas en los comentarios en este artículo de Erica Sadum: Swift reader documentación in Xcode 7.

Cerrando la Inicialización en viewWillAppear(animated: Bool)

El ultimo paso para dejar la aplicación lista para ser usada por el usuario es establecer los botones de la interface en el estado inicial esperado:

  • Grabar: habilitado
  • Reproducir: deshabilitado
  • Detener: deshabilitado
  • Pausa: deshabilitado

La acción más adecuada para hacer esta tarea es viewWillAppear(animated: Bool) que se ejecuta justo antes que se muestre la vista.

En este segmento de código sólo llamamos al método setStateForUIButtons(recordButton: true, playButton: false, stopButton: false, pauseButton: false) para establecer su estado de acuerdo a los estados antes mencionados.

Las Acciones

Una vez lista la aplicación debemos programar el comportamiento para los botones grabar, reproducir, detener y pausa. Asegúrense de haber establecido todas las acciones pertinentes presionando ctrl-clic sobre los botones y arrastrando el cursos hacia la ventana de código de nuestro controlador: viewController.

Grabar

El método recordSound(sender: UIButton) es una acción relacionada con el botón de grabar, y su función es la de iniciar el proceso de grabación llamando al método .record() del objeto AVAudioRecorder como se muestra en el siguiente código.

También debe de encargarse de modificar el estado de los botones y de la etiqueta de texto de acuerdo.

Reproducir

El método playSound(sender:UIButton) es el que controla el comportamiento de la aplicación cada vez que se hace tap en el botón de reproducir.

Aquí creamos una instancia de la clase AVAudioPlayer apuntando al mimos URL usado por la instancia de AVAudioRecorder, de esta manera se reproducirá el audio previamente grabado.

Como en Swift 2.0 el constructor de AVAudioPlayer esta preparardo para devolver un error a través de un Throw (Ver la documentación de Apple sobre manejo de errores en Swift 2.0) debemos encapsular nuestro código en un do…try…catch.

Detener

Esta porción de código es simple, si estamos grabando sólo llamamos al método .stop() de nuestra instancia de AVAudioRecorder, caso contrario llamamos al método .stop() de nuestra instancia de AVAudioPlayer, asegurándonos solo de establecer la propiedad .currentTime a 0.0 para que no se comporte como una pausa.

Pausar

Aunque el estado de los botones debe de evitar el caso en que mientras se esté grabando se haga una actividad de pausado y cambie el estado de la interfase no está demás poner un doble seguro así que solo sí no estamos grabando procedemos a cambiar el estado de los botones y a llamar el método .pause() de nuestra instancia de AVAudioPlayer y actualizamos la etiqueta para anunciar que estamos en el estado de pausa.

Delegados

Los delegados son métodos que las instancias de AVAudioRecorder y AVAudioPlayer llamarán cuando ciertos eventos ocurran. En nuestro caso implementamos los siguientes para poder actualizar adecuadamente el estado de la interface cuando tanto AVAudioRecorder para de grabar o AVAudioPlayer para de reproducir por razones ajenas a las interacciones con el usuario. Esto se puede deber a una llamada entrante o por haberse quedado sin espacio por ejemplo.

Para terminar (las notas del cajón)

Esta aplicación no tiene aun el nivel necesario para ser subida a la Apple Store, le faltan muchos detalles que refinar y algunas mejoras o adiciones a la funcionalidad. La siguiente es una lista que me viene a la mente de lo que aun se necesita hacer y que espero sean motivo para un siguiente artículo.

  1. Internacionalización de la interface para que sea capaz de acomodarse a diversos lenguajes.
  2. Manejar las posibles interrupciones como las originadas por llamadas entrantes y recuperarse adecuadamente de ellas.
  3. Ser capaz de manejar múltiples grabaciones en simultáneo.
  4. Sincronizar grabaciones entre dispositivos.
  5. Agregar una onda de sonido (animada) al momento de reproducir.
  6. Agregar un medidor de audio al momento de grabar.
  7. Agregar etiquetas que muestren el largo del audio y el tiempo en que se encuentra la reproducción.
  8. Además de un par de revisiones al código para hacerlo más eficiente  (refactorización).

¿Qué más se les ocurre? Espero sus comentarios.