Tecnófilos
  Inicio       Proyectos       Energías Renovables       Manuales             Videos      

Foro

      Contacto  
   

 

Cómo programar la tarjeta de sonido en C++ Builder para capturar formas de onda.

 

Se expone cómo programar la tarjeta de sonido para capturar formas de onda. Estas formas de onda pueden sernos útiles para crear instrumentos virtuales tales como osciloscopios, medidores de impedancia digital etc. De esta forma, podemos realizar proyectos complejos con una mínima cantidad de hardware, aprovechando la potencia y velocidad de una ordenador moderno sobre todo en los cálculos matemáticos. Implementar lo mismo en microcontroladores requiere una esfuerzo elevado en la creación de un hardware más elaborado y unas funciones matemáticas complejas a medida para cada tipo de microcontrolador.

Grabando los datos en formato de audio WAVE.

Enumeramos a continuación los pasos que se requieren para programar correctamente una captura de audio con la tarjeta de sonido.

Establecer el formato de audio WAVE.

Abrir el dispositivo de entrada de onda WAVE.

Preparar la cabecera WAVE.

Empezar la grabación.

Cerrar el dispositivo cuando la grabación finaliza.

Empaquetamiento de los datos para archivos PCM.

Formato de datos de la muestra.

 

Establecer el formato de audio WAVE.

Antes de abrir el dispositivo, debemos establecer los parámetros de grabación de los datos de audio en formato wave. Deberemos rellenar los campos de una estructura llamada WAVEFORMATEX, declarada en mmsystem.h. Los datos de audio están en formato PCM ( pulse code modulation ). Los datos PCM tienen tres propiedades que determinan la calidad del sonido grabado: el número de bits por muestra, el número de muestras por segundo, y el número de canales. Se puede establecer los bits por muestra a 8 o 16 bits. El número de muestras utilizado comúnmente es 8000; 11025, 22050, 44100, o 48000. El número de canales puede ser 1 ( mono ) o 2 ( estéreo ).

El tamaño de los datos almacenados en el disco duro es directamente proporcional a la calidad del audio. Diez segundos de audio grabados a 8 kHz, mono, y 8 bits por muestra, producen un fichero WAV de 79 kB. Diez segundos de audio grabado a 44,1 kHz, estéreo, y 16 bits por muestra, sin embargo, resulta en un tamaño aproximado de 862 kB. En general un formato de onda de 22,05 kHz, 8 bits por muestra, y mono, tiene un compromiso razonable entre calidad de sonido y espacio almacenado en disco. ( el sistema Windows almacena sus sonidos en este formato ).

Después de haber decidido que formato de onda WAVE vamos a utilizar, necesitamos crear la variable tipo WAVEFORMATEX y rellenar sus campos. Aquí tenemos el código.:

// Declaramos la variable WaveFormat de tipo WAVEFORMATEX.
WAVEFORMATEX WaveFormat;

// Rellenamos los campos.
WaveFormat.wFormatTag = WAVE_FORMAT_PCM;
WaveFormat.nChannels = 1;
WaveFormat.nSamplesPerSec = 22050;
WaveFormat.wBitsPerSample = 8;
WaveFormat.nAvgBytesPerSec = 22050;
WaveFormat.nBlockAlign = 1;
WaveFormat.cbSize = 0;

Advierta que el campo wFormatTag está establecido a WAVE_FORMAT_PCM. Este es el formato de Windows para los archivos WAV. ( Otros formatos soportados están definidos en MMREG.h, por si quieren ser revisados ). Los campos nChannels, nSamplesPerSec, y nBitsPerSample establecen el formato PCM. El campo nAvgBytesPerSec se establece para la media de bytes grabados o muestreados por segundo. Este valor se determina con la siguiente ecuación:

nAvgBytesPerSec = ( SamplesPerSecond * Channels * BitsPerSample ) / 8

El campo nBlockAlign también requiere explicación. Se determina su valor mediante la siguiente ecuación:

nBlockAlign = (Channels * BitsPerSample) / 8

El campo cbSize especifica el número de bytes extras de datos almacenados con la cabecera de formato WAVE. Este valor normalmente no se utiliza cuando estamos grabando datos WAVE.

 

Abrir el dispositivo de entrada de onda WAVE.

Antes de abrir el dispositivo para la grabación primero debemos determinar que la tarjeta de sonido instalada pueda soportar el formato seleccionado. Se realiza llamando al procedimiento waveInOpen() win el parámetro WAVE_FORMAT_QUERY. Por ejemplo:

int Res = waveInOpen(&WaveHandle,
WAVE_MAPPER, &WaveFormat, 0, 0,
WAVE_FORMAT_QUERY);
if (Res == WAVERR_BADFORMAT) return;

Si el formato de onda ( pasado en la estructura WaveFormat in el tercer parámetro ) especificado es compatible, waveInOpen() devolverá el valor 0. Si el formato es incompatible con la tarjeta instalada, waveInOpen() devolverá el código de error WAVERR_BADFORMAT. Cuando ejecutamos este fragmento de código, Windows chequea las capacidades de la tarjeta de sonido, pero no abre el dispositivo para la grabación. De esta forma tenemos una forma de detener el proceso de grabación si el formato especificado no está soportado por el hardware.

Ahora que conocemos que el formato es válido, debemos de abrir el dispositivo de grabación. Aquí tenemos el código:

Res = waveInOpen( &WaveHandle, WAVE_MAPPER, &WaveFormat, MAKELONG(Handle, 0), 0, CALLBACK_WINDOW);

En este ejemplo, WaveHandle es una variable ( de tipo HWAVEIN ) que recibe el manejador de dispositivo si se ha abierto correctamente. La constante WAVE_MAPPER le dice a Windows que use el primer dispositivo de grabación disponible en el sistema que pueda soportar el formato especificado ( que es usualmente la tarjeta de sonido ). El cuarto y sexto parámetro de waveInOpen() le dice a Windows que envie los mensajes generados por la tarjeta de sonido a la aplicación que está haciendo uso de ella. El quinto parámetro pasa información adicional a la aplicación cuando Windows envía los mensajes de la tarjeta de sonido. Puesto que no utilizamos en nuestra aplicación le damos el valor 0 a este parámetro. Si waveInOpen() devuelve el valor 0, entonces podemos proceder ahora con el siguiente paso: crear el buffer de memoria para la grabación de sonido.

 

Reservando espacio de memoria para el buffer de grabación.

Lo primero que debemos saber es cuanta memoria vamos a gastar para reservar el buffer. Se puede calcular su tamaño basándonos en el número de segundos que queremos grabar mediante la siguiente ecuación:

RecordSeconds * AvgerageBytesPerSecond

Por ejemplo, si quieres grabar 10 segundos de datos a 22,05 kHz, mono, 8-bit, podrías usara el siguiente código para alojar el buffer:

// Declaración de tipo de datos para el buffer.
char* WaveData;
int BufferSize;

BufferSize = 10 * 22050;
WaveData = new char[BufferSize];

Recuerde liberar la memoria del buffer de grabación cuando se termine el programa.

 

Preparando la cabecera WAVE.

Ahora estamos listos para preparar la cabecera de datos del archivo tipo WAVE, una estructura que contiene información que Windows necesita para llevar a cabo la grabación. Específicamente almacena el tamaño del buffer de grabación y un puntero al mismo. ( No confundir la cabecera de entrada con el formato WAVE de la cabecera ). El formato WAVE de la cabecera es un ejemplo de una estructura WAVEHDR, esta estructura se usa cuando ejecutamos una grabación o una reproducción, así que podemos ignorar muchos de sus campos cuando grabamos datos. Para la grabación solamente necesitamos establecer los siguientes campos de la estructura: dwBufferLength, dwFlags, and lpBuffer. Por ejemplo:

//Declaración de la variable WaveHeader de tipo WAVEHDR.
WAVEHDR WaveHeader;

WaveHeader.dwBufferLength = BufferSize;
WaveHeader.dwFlags = 0;
WaveHeader.lpData = WaveData;

Advierta que los campos dwBufferLength y IpData asignan valores obtenidos cuando alojábamos el buffer de memoria en el paso previo. Debemos establecer a 0 el campo dwFlags para la grabación. Ahora que la cabecera WAVE ha sido establecida podemos utilizar el procedimiento waveInPrepareHeader():

Res = waveInPrepareHeader( WaveHandle, &WaveHeader, sizeof(WAVEHDR) );

Si waveInPrepareHeader() devuelve el valor 0, la cabecera está lista para ser utilizada. Para añadir el buffer de grabación llamamos a la función waveInAddBuffer() tal como sigue:

Res = waveInAddBuffer( WaveHandle, &WaveHeader, sizeof(WAVEHDR) );

Esta función añade el buffer a la lista de buffers especificados en la cabecera que serán reproducidos. Como usamos un solo buffer en este caso, solamente se realiza un paso. Por cada buffer que deseemos añadir deberemos repetir los mismos pasos.

 

Empezar la grabación.

Estamos listos para grabar. Se realiza llamando a la función waveInStart(), pasando el manejador para la tarjeta de sonido ( obtenido cuando llamamos a waveInOpen() ) Esta parte es sencilla:

Res = waveInStart(WaveHandle);

La función waveInStart() comienza la grabación y devuelve inmediatamente el control a la aplicación. Si waveInStart() devuelve el valor 0, la grabación ha empezado correctamente. El proceso de grabación se realiza de forma asíncrona, esto quiere decir que la aplicación en curso puede realizar otras tareas mientras se esté grabando. Esto nos da la posibilidad de parar el proceso de grabación mediante un botón u otro evento.

 

Capturando los mensajes de la tarjeta de sonido.

Para hacer algo útil con los datos capturados, necesitamos determinar cuando la grabación ha finalizado. Para realizar esto podemos capturar los mensajes de Windows que genera la tarjeta de sonido.

Los mensajes de la tarjeta de sonido referentes a grabación son los siguientes.

MM_WIM_OPEN: El dispositivo ha sido abierto.
MM_WIM_CLOSE: El dipositivo ha sido cerrado.
MM_WIM_DATA: La grabación ha finalizado y el buffer de grabación se devuelve a la aplicación.

De estos mensajes el que nos interesa es el concerniente a MM_WIM_DATA. De hecho debemos responder a este mensaje si vamos a utilizar los datos de grabación. MM_WIM_DATA se recibe cuando los datos del buffer se devuelven a la aplicación. Esto puede ocurrir como resultado de dos eventos primarios: O el buffer de memoria se ha llenado o la grabación se ha interrumpido. En ambos casos el mensaje notifica que el proceso de grabación ha finalizado. En este punto podemos usar los datos capturados para el programa o grabarlos en un fichero WAV.

 

Capturando el mensaje MM_WIM_DATA.

Puedes capturar el mensaje MM_WIM_DATA implementando un mapa de mensajes en C++ Builder. Este mensaje tiene el siguiente aspecto:

BEGIN_MESSAGE_MAP
MESSAGE_HANDLER(
MM_WIM_DATA, TMessage, OnWaveMessage)
END_MESSAGE_MAP(TForm)

Abrimos el archivo include correspondiente al formulario principal. Insertamos las líneas anteriores en la parte public: después de la siguiente línea:

__fastcall TForm1(TComponent* Owner);

La función OnWaveMessage() deberá ser declarada dentro de la parte private: del formulario principal, añadiendo la siguiente línea:

void OnWaveMessage(TMessage& msg);

Si no se realizan estas dos acciones el mensaje no podrá ser gestionado por la aplicación en curso. El manejador de mensajes OnWaveMessage() será llamado cuando se reciba un mensaje MM_WIM_DATA procedente de la tarjeta de sonido. En este punto realizamos la acción apropiada que necesitemos.

 

Cerrar el dispositivo cuando la grabación finaliza. El procedimiento manejador del mensaje MM_WIM_DATA.

Este manejador necesita realizar tres cosas: 1º Cerrar el dispositivo de grabación. 2º Salvar los datos al disco ( opcional si se quiere una copia en WAV ). 3º Liberar la memoria usada por el buffer de grabación.

Aquí tenemos un ejemplo de código que puede realizar todas estas acciones.

void TMainForm::OnWaveMessage(TMessage& msg){

if (msg.Msg == MM_WIM_DATA) {
// Cierra el dispositivo de audio.
waveInClose(WaveHandle);
// Salva los datos al disco.
SaveWaveFile();
// Libera la memoria del buffer de grabación.
WaveHeader.lpData = 0;
delete WaveData;
WaveData = 0;
}
}

Este código es sencillo. Primero, el procedimiento waveInClose() cierra el dispositivo de grabación utilizando el manejar obtenido cuando fue abierto. Después, el procedimiento SaveWaveFile() almacena los datos en disco en formato *.wav. Finalmente la memoria reservada para el buffer es liberada. El manejador para el mensaje MM_WIM_DATA es sencillo, pero es una parte vital para las operaciones de grabación.

 

Salvar los datos al disco duro.

Para guardar los datos al disco duro puede implementarse una función como la que sigue:

void TMainForm::SaveWaveFile(){

// Declara los objetos que necesitaremos.
MMCKINFO ChunkInfo;
MMCKINFO FormatChunkInfo;
MMCKINFO DataChunkInfo;

// Abre el archivo.
HMMIO handle = mmioOpen( "test.wav", 0, MMIO_CREATE | MMIO_WRITE );
if (!handle) {
MessageBox(0, "Error creating file.", "Error Message", 0);
return;
}

// Crear el segmento RIFF.
memset(&ChunkInfo, 0, sizeof(MMCKINFO));
ChunkInfo.fccType = mmioStringToFOURCC("WAVE", 0);
DWORD Res = mmioCreateChunk(
handle, &ChunkInfo, MMIO_CREATERIFF);
CheckMMIOError(Res);

// Crear el formato de segmento.
FormatChunkInfo.ckid = mmioStringToFOURCC("fmt ", 0);
FormatChunkInfo.cksize = sizeof(WAVEFORMATEX);
Res = mmioCreateChunk(handle, &FormatChunkInfo, 0);
CheckMMIOError(Res);
// Escribe los datos WAV.
mmioWrite(handle, (char*)&WaveFormat, sizeof(WaveFormat));

// Crea los datos del segmento.
Res = mmioAscend(handle, &FormatChunkInfo, 0);
CheckMMIOError(Res);
DataChunkInfo.ckid = mmioStringToFOURCC("data", 0);
DataSize = WaveHeader.dwBytesRecorded;
DataChunkInfo.cksize = DataSize;
Res = mmioCreateChunk(handle, &DataChunkInfo, 0);
CheckMMIOError(Res);

// Escribe los datos.
mmioWrite(handle, (char*)WaveHeader.lpData, DataSize);
mmioAscend(handle, &DataChunkInfo, 0);

mmioAscend(handle, &ChunkInfo, 0);
mmioClose(handle, 0);
}

La función utilizada en este procedimiento se llama CheckMMIOError(Res) y sirve para obtener el código de error que se pudiera realizar durante las operaciones. Tiene el siguiente contenido:

void TMainForm::CheckMMIOError(DWORD code)
{
// Informa de un error tipo mmio, si ocurriera.
if (code == 0) return;
char buff[256];
wsprintf(buff,"MMIO Error. Error Code: %d", code);
Application->MessageBox(buff, "MMIO Error", 0);
}

 

Empaquetamiento de los datos para archivos PCM.

En un archivo con un solo canal, las muestras están almacenadas consecutivamente. Para archivos estéreo WAVE, el canal 0 representa el canal izquierdo, y el canal 1 representa el canal derecho. Los siguientes diagramas muestran el empaquetamiento para archivos WAVE de 8-bit mono y estéreo:

Para 8 Bits:

Muestra 1
Muestra 2
Muestra 3
Muestra 4
Canal 0
Canal 0
Canal 0
Canal 0

Encapsulamiento para 8-bit Mono PCM.

Muestra 1
Muestra 2
Canal 0 izq.
Canal 1 der.
Canal 0 izq.
Canal 1 der.

Encapsulamiento para 8-bit Estéreo PCM.

Para 16 Bits:

Muestra 1
Muestra 2
Canal 0
Canal 0
byte bajo
byte alto
byte bajo
byte alto

Encapsulamiento para 16-Bit Mono PCM.

Muestra 1
Canal 0 Izq.
Canal 1 Der.
byte bajo
byte alto
byte bajo
byte alto

Encapsulamiento para 16-Bit Estéreo PCM.

 

Formato de datos de la muestra.

Cada muestra está contenida en un entero i. El tamaño de i es el número más pequeño de bytes requerido para contener el tamaño de la muestra actual. El byte menos significativo se almacena primero. Los bits que representan la amplitud de las muestras se almacenan en los bits más significativos de i, y el resto de bits se ponen cero.

Por ejemplo, si el tamaño de la muestra ( almacenado en el campo nBitsPerSample ) es de 12 bits, entonces cada muestra es almacenada en dos enteros de byte. Los cuatro bits menos significativos del primer byte se ponen a cero.

Los formatos de dato y los valores máximos y mínimos para las muestras de formas de onda PCM de varios tamaños son los siguientes:

 

Tamaño de muestra Formato de datos Valor máximo Valor mínimo
Uno a 8 bits
Entero sin signo i
255 (0xFF)
0
9 o más bits
Entero con signo i
Valor más positivo de i
Valor más negativo de i

 

Por ejemplo, el máximo, mínimo, y punto medio para valores de 8-bit y 16-bit de formas de onda PCM son los siguientes:

 

Formato Maximo Valor Mínimum Valor Valor Medio
8-bit PCM
255 (0xFF)
0
128 (0x80)
16-bit PCM
32767 (0x7FFF)
-32768 (-0x8000)
0