6 Canales de Audio
En esta sección, describiremos las clases audio channel utilizadas en la aplicación tutorial. Por defecto, la funcionalidad del stack OpenH323 es la de leer (y escribir) datos de audio desde (o hacia) una tarjeta de sonido. Nuestra aplicación necesita sobreescribir esta funcionalidad por defecto, además de tener que definir nuestros propios canales de audio.
La aplicación oh323tut utiliza dos clases channel, ambas derivadas de la clase PIndirectChannel: WavChannel (Archivos wavchan.h ytcz_tt1( wavchan.cxx)) y NullChannel (archivos nullchan.h y nullchan.cxx). Al estudiar el código fuente, seguramente te darás cuenta que la funcionalidad de estas dos clases pueden ser combinada en una sola clase. Tenemos las dos clases separadas para poder facilitar el entendimiento del código. Esto también ayuda a enfatizar que el audio entrante y saliente son independientes el uno del otro.
Al definir las nos nuevas clases channel (canal), necesitamos sobreescribir cuatro métodos virtuales: Close(), IsOpen(), Read(), y Write(). Encontrarás los prototipos de estos métodos en el archivo wavchan.h (líneas 46-49) o en nullchan.h (líneas 45- 48), respectivamente . El rol de cada uno de los cuatro métodos virtuales es fácil de entender. Una vez que una instancia de channel (canal) es creada, se espera que sea abierta. El método IsOpen() es utilizado para verificar que todo lo ocurrido durante la creación del canal se realizó sin problemas (p.e. inicialización de dispositivo, apertura de archivos), Close() es invocado cuando el canal ya no se necesita.. Read() y Write() son utilizados para leer/escribir datos desde/hacia la instancia del canal.
6.1 WavChannel
El objetivo de la clase WavChannel class es la de leer datos de audio desde un archivo WAV. La declaración de WavChannel empieza en la línea 36 en el archivo wavchan.h. La clase necesita cuatro datos miembro. El miembro myConnection es una referiencia a la clase H323Connection - necesitamos la referencia p.e.para cerrar la coneción cuando llegamos al fin del archivo WAV. PWAVFile wavFile es un objeto que nos permite leer datos de audio desde un archivo WAV. Los dos miembros restantes writeDelay y readDelay son ambos de tipo PAdaptiveDelay. Explicaremos el rol del delay (retardo) adaptativo en detalle posteriormente.
Ahora describamos la implementación de los métodos de WavChannel.
6.1.1 Constructor y Destructor
El constructor de WavChannel (archivo wavchan.cxx, líneas 30-52) toma dos parámetros formales: una referencia al nombre de un archivo WAV y una referencia a un objeto H323Connection. Estos dos parámetros son utilizados por los inicializadores de los datos miembros en la línea 31. El constructor de PWAVFile intenta abrir el archivo, por tanto, el primer paso que debemos realizar dentro del constructor es verificar si el archivo ha sido abierto exitosamente (líneas 33-38). Si el archivo no ha sido abierto, desplegamos un mensaje de error, cerramos la conexión H.323 y salimos del constructor.
Nuestro siguiente paso dentro del constructor (líneas 39-48), es verificar si el archivo WAV tiene los parámetros necesarios, p.e. el formato es PCM, mono (un solo canal de audio), el sampling rate (muestreo) es de 8000 Hz, y el tamaño de muestra (sample size es de 16 bits). Si el archivo no cumple con estos requerimientos, hacemos lo mismo que hicimos anteriormente, p.e. desplegar un mensaje de error y cerrar la conexión H.323.
La última línea en el constructor es simplemente una instrución PTRACE que indica la creación exitosa del objeto WavChannel.
La única instrucción en el destructor de WavChannel (líneas 57-60) es, nuevamente, una instrucción PTRACE que informa acerca de la eliminación del objeto channel.
6.1.2 Close() e IsOpen()
La implementación de los métodos Close() (líneas 65-68) e IsOpen() (líneas 73-77) de WavChannel es muy simple - retornan los valores verdaderos obtenidos desde Close() y IsOpen() de wavFile, respectivamente.
6.1.3 Acciones comunes requeridas en los métodos Read() y Write()
Además del proceso real de datos de audio, ambos métodos, Read() y Write() , deben considerar dos aspectos:
- Notificar a quien solicita la llamada (caller) sobre el resultado de la operación de lectura/escritura;
- Ocuparse de una temporización correcta.
Notificando acerca del resultado de una operación
Para notificar al canal del usuario acerca del resultado de una operación de lectura/escritura, tenemos que considerar dos aspectos. Primero, debemos configurar la variable miembro de channel lastWriteCount (en Write()) o lastReadCount (en Read()) al número de bytes exitosamente escritos o leídos. Luego de retornar de Read() oWrite(), este número puede ser obtenido desde los métodos GetLastWriteCount() y GetLastReadCount() de channel.
El segundo aspecto, es que Read() y Write() deberían retornar true o false según los requerimientos pre-definidos en pwlib/include/ptlib/channel.h . Write() debería retorna true si es que pudo escribir todos los bytes indicados, en caso contrario, false. Read() debería retornar true si al menos un byte fue leído, false en caso contrario.
Temporización
Además de notificar sobre el éxito o fracaso de las operaciones de lectura/escritura, debemos preocuparnos por la temporización. Cuando utilizamos una tarjeta de sonido como la fuente o destino de los datos de audio, la tarjeta nos puede proporcionar una temporización muy precisa. Por ejemplo, si leemos 80 muestras (samples) con la frecuencia de muestreo (sampling frequency) de 8000 muestras por segundo, la operación de lectura terminará (casi exactamente) 10 milisegundos luego del final de la lectura previa. La temporización es esencial, especialmente, para el método Read(), ya que influye en la calidad del audio en la parte que lo recibirá. A pesar de que los endpoints cuentan con buffers para controlar el jitter (retardo), deberíamos enviar paquetes RTP de la forma mas exacta posible.
No podemos depender de la tarjeta de sonido en WavChannel o NullChannel, sin embargo el tiempo transcurrido dentro de Read() oWrite() debería, nuevamente, corresponder a la cantidad de datos leídos o escritos. Para lograr esto, debemos adicionar cierto tiempo de retardo (sleep). Así, por ejemplo, si Write() es invocado con 480 bytes (p.e. 240 muestras - correspondiendo esto a 30 milisegundos) y suponiendo que el proceso solamente necesita , digamos, 1 milisegundo dentro de Write() y 1 milisegundo entre dos invocaciones consecutivas a Write(), el retardo (sleep) adicional debería durar los restantes 28 milisegundos, para asegurar que el tiempo entre dos llamadas consecutivas a Write() sea de 30 milisegundos.
El problema con la función sleep (retardo) en la mayoría de los sistemas (tanto Unix/Linux y Windows) es que no es exacto. Usualmente redondea los tiempos a múltiplos de 10 milisegundos. Entonces, el error de redondeo puede variar de 0 a 9 milisegundos y esto es una mala noticia si consideramos que una invocación típica a Read() oWrite() trabaja con 80, 160, o 240 muestras, correspondiendo a 10, 20, o 30 milliseconds, respectivamente. Suponiendo que la función de retardo sleep fuera correcta, acumularíamos un error considerable incluso luego de pocas invocaciones a Read() oWrite(). Necesitamos utilizar un algoritmo de retardo adaptativo (adaptative delay), de tal manera que incluso causando un error de temporización durante una invocación a Read() oWrite(), este error sea compensado en la(s) llamada(s) subsecuente(s) . De esta manera, el tiempo de partida no será exacto para cada paquete individual RTP, sin embargo, el tiempo promedio de llegada entre dos paquetes será aproximado a un valor exacto.
PWLib implementa un algoritmo de retardo adaptativo en una clase denominada PAdaptiveDelay. La clase utiliza el concepto de "target time". Cuando el método Delay(int time) de PAdaptiveDelay es invocado por primera vez (time es expresado en milisegundos), el "target time" es configurado al tiempo actual sumándole time milisegundos. Durante las invocaciones subsecuentes a Delay(), eñ "target time" es simplemente es incrementado en time milisegundos. Luego de ajustar el "target time", el algoritmo calcula la diferencia entre el "target time" y el tiempo actual, y luego realiza un retardo (sleepr) en tiempo resultante de esta diferencia.
El utilizar "target time" (valor absoluto) nos ayuda a evitar la acumulación de errores de tiempo. Supóngase que Ti es el "target time" la iteración número i, N es el tiempo actual (Ahora) y e es el error por el retardo (sleep). Si , en la iteración número i, el retardo toma Ti - N + e, la iteración número i finaliza en el tiempo Ti + e, en lugar de (idealmente) Ti. En la siguiente iteración número (i+1), el tiempo actual (N) será aproximadamente igual a Ti + e, de tal manera que la duración del retardo será computa como Ti+1 - N = Ti+1 - Ti - e y el error será compensado. Nuevamente, el retardo en la iteración número (i+1) podría no ser exacto y esto podría ser corregido en la siguiente iteración (i+2), y así sucesivamente. De esta manera, la duración promedio de una iteración debería ser cercana al tiempo ideal.
6.1.4 Write()
El principal objetivo de la clase WavChannel es el de leer datos desde un archivo de audio. Por esta razón, el método Read() es más importante que Write(). De hecho, WavChannel::Write() nunca debería ser invocada en nuestra aplicación, ya que el método MyEndPoint::OpenAudioChannel() asigna una instancia de WavChannel al hilo (thread) responsable del audio saliente. Sin embargo, sí implementaremos WavChannel::Write() (archivotcz_tt1( wavchan.cxx), líneas 82-88). Aprovecharemos la oportunidad para demostrar los poco pasos que son requeridos para los métodos Read() oWrite() de cada canal.
Nuestro WavChannel::Write() simplemente ignorará cualquier dato
que se le sea pasado, pero "fingirá" que el buffer de datos ha
sido escrito exitosamente. De hecho, este es el mismo comportamiento de /dev/null
en Unix. El tamaño del buffer (PINDEX len, el segundo
parámetro de Write()) está dado en bytes. Primero
definiremos la variable miembro de channel lastWriteCount a un
número de bytes (correctamente) escritos (archivo wavchan.cxx,
línea
85). Luego de esto, invocaremos a un retardo adaptativo llamando a:
writeDelay.Delay(len/2/8);
El objeto writeDelay es una instancia de la clase PAdaptiveDelay (referirse 6.1.3 en párrafos anteriores). El método Delay() recibe la duración del retardo expresada en milisegundos. Para obtener el número de milisegundos, simplemente dividimos len (que es el tamaño del buffer en bytes) entre 2 dado que cada muestra (sample ) ocupa 2 bytes (16 bits) y luego entre 8 ya que hay exactamente 8 muestras en un milisegundo (el sampling rate es de 8000 Hz).
El último paso dentro de Write() es el de retornar true para notificar a la parte que llama (caller) que el todo el buffer fue procesado de manera exitosa (nuevamente, referirse a 6.1.3 en párrafos anteriores).
6.1.5 Read()
Tratemos ahora el método Read() de WavChannel (líneas 93-117). Su objetivo es de leer datos de audio desde un archivo WAV.
El código en las líneas 95 a 102 asegura que los canales trabajarán correctamente con un inicio adelantado de transmisión del medio, cuando los canales lógicos sean iniciados antes que el endpoint que llama envíe CONNECT. Bien podría pasar que quisieramos mandar los primeros segundos del archivo WAV y el receptor aún o estaría preparado para escucharlos. Para evitar esto, verificamos (línea 95) si la conexión H.323 ha sido establecida, y si no, rellenamos el buffer con silencios (bytes a cero) en lugar de los datos reales del archivo. Naturalmente, necesitamos realizar los pasos requeridos, p.e. asignar un valor a lastReadCount (línea 99) y controlar la temporización correcta (línea 100). Retornamos del método con true en la línea 101 - la parte restante del método será ejecutada solamente cuando la conexión sea establecida..
Leemos datos de audio desde un archivo en la línea 104 y si esta lectura falla, retornamos false inmediatamente. Si la operación de lectura del archivo es exitosa, asignamos a la variable lastReadCount de channel el valor obtenido desde el método LastReadCount() de wavFile y luego (línea 108) invocamos un retaedo adaptativo (lastReadCount/2/8 equivale al número de milisegundos que corresponden al número de muestras leídas desde un archivo, ver también 6.1.3 y 6.1.4).
Las líneas de 110 a 114 se ocupan de la situación cuando la operación de lectura retorna menos datos que la cantidad solicitada (p.e. len). Se espera que esto pase cuando se llega al final del archivo de audio. Deseamos colgar la llamada H.323 en este momento, así que invocamos a myConnection.ClearCall() en la línea 113. Nótese que ClearCall() no realiza toda la culminación de la conexión. Solamente inicia el fin de la llamada y sale (retorna), por tanto, el método Read() tendrá tiempo para ejecutarse hasta que finalice. Las acciones para la terminación de la llamada son realizadas en paralelo por otro hilo (thread).
El método termina en la línea 116 con una instrucción que retorna true si al menos un byte fue leído, conforme al requerimiento en pwlib/include/ptlib/channel.h (ver también 6.1.3).
6.2 NullChannel
La clase NullChannel pretende comportarse tal como /dev/null en Unix para escribir y /dev/zero para leer. El código de esta clase es bastante simple y reutiliza parte del código de WavChannel, así que no lo describiremos en detalle.
El método Write() de NullChannel es similar que WavChannel::Write(). Ignora los datos que se le son pasados, pero reporta que éstos fueros exitosamente escritos - referirse a la sección 6.1.4 en párrafos anteriores. El método Read() de NullChannel llena el buffer que se le es indicado con silencio (bytes a cero). Ambos, Read() y Write() utilizan retardo adaptativo - referirse a las secciones 6.1.3 a la 6.1.5.
Siguiente: Ver código fuente y realizar descargas (download)