Aula Macedonia


Artículos Varios


Artículo realizado por
Iñaki Ecenarro.






Introducción a DirectSound

Después del artículo del mes pasado sobre DirectDraw ahora le toca el turno al DirectSound que tampoco tiene demasiada complicación.

Antes que nada, deciros que ya he probado la versión 5 de DirectX, y he probado el ejemplo del mes pasado y se compila y funciona perfectamente con la nueva versión. Además la nueva versión trae librerías para Borland C++, o sea que los que usamos Borland lo tenemos ahora un poco más fácil.

Bueno, en este artículo vamos a aprender un poco de DirectSound, que es bastante sencillo. Al final haremos un sencillo ejemplo que tocará una música de fondo (premio para el que adivine de dónde he sacado la música) y con los botones del ratón se harán los efectos. Por cierto, el ejemplo lo he hecho utilizando Borland C++ 4.51, pero no usa ninguna característica específica de ese compilador, por lo que supongo que no habrá ningún problema para compilarlo con Visual C++ o Watcom.
Bueno, empezamos.

1. Inicialización de DirectSound:

Como ocurría con DirectDraw, lo primero que tenemos que hacer es inicializar DirectSound.o primero que tenemos que hacer es inicializar DirectDraw. Como hice en el ejemplo de DirectDraw, he agrupado todo lo relativo a la inicialización en la función DirectSoundInit(), que devuelve un valor VERDADERO si no encuentra ningún problema, y FALSO si hay algún problema. En este último caso, antes de cerrar el programa se presenta un mensaje al usuario.

Por cierto, el ejemplo no funciona si ocurre algún problema al inicializar DirectDraw, pero en un juego real se podría seguir ejecutando el juego pero sin utilizar el sonido, simplemente habría que crear una variable global que indicara si ha habido algún problema con DirectSound, para no llamar a ninguna función de DirectSound si ha habido algún problema al principio. Bueno, esto es bastante elemental y supongo que todo el mundo lo habría pensado antes de leer este párrafo, pero por si acaso ahí queda.

Vamos a ver qué es lo que hay dentro de la función DirectSoundInit(). Lo primero que hay que hacer es crear un objeto DirectSound:


LPDIRECTSOUND lpDD; // este es el objeto DirectSound
HRESULT ddrval;     // esta variable contendrá el valor devuelto
                    // por las funciones DirectX

hr = DirectSoundCreate(NULL, &lpDirectSound, NULL);

if( hr != DS_OK ) 
return CleanupAndExit("Error en DirectSoundCreate!");

La función CleanupAndExit() es una función que hemos creado nosotros, que lo que hace es borrar todos los objetos DirectSound creados y luego presentar al usuario un mensaje de error. Utilizaremos la variable ddrval para almacenar el valor devuelto por las distintas funciones de DirectSound. Si el valor es DD_OK no ha habido ningún problema. En caso contrario, ddrval nos indicará cuál ha sido el problema (aunque en este caso no lo analizamos, en un programa serio debería hacerse).

El párrafo anterior lo he copiado del artículo de DirectDraw, cambiando Draw por Sound :-)

Ahora tenemos que establecer el nivel de cooperación de nuestra aplicación:


hr = lpDirectSound->SetCooperativeLevel(hwnd, DSSCL_NORMAL);

if( hr != DS_OK) 
return CleanupAndExit("Error en SetCooperativeLevel!");

Los distintos valores posibles para el segundo parámetro de la función SetCooperativeLevel() son:

En la ayuda de DirectX se recomienda que para la mayoría de las aplicaciones el mejor parámetro debe ser DDSCL_NORMAL, dice que es mejor en un entorno multi-tarea como es Windows (???). Si usas otros parámetros tienes acceso más directo al hardware, y dice que eso puede dar problemas cuando el usuario cambie entre distintas aplicaciones activas. No lo sé, yo sólo he probado el parámetro DDSCL_NORMAL, pero lo que se me ocurre es que si el usuario está con un juego seguramente no querrá cambiar a otra aplicación, o sea que no creo que hay ningún problema utilizando los otros parámetros. Es cuestión de probar.

Ahora podemos obtener las "capabilities" ( ¿capacidades? ) del hardware instalado. Esto no es necesario, pero puede ser interesante para obtener datos del sistema en el que se está ejecutando el programa, para por ejemplo tocar más o menos sonidos en función del hardware instalado.



DSCAPS dscaps;

dscaps.dwSize = sizeof( DSCAPS );
lpDirectSound->GetCaps( &dscaps )

2. Crear los buffers de  sonido

El siguiente paso es crear los buffers de sonido. Al inicializar DirectSound se crea automáticamente el buffer de sonido primario, usado para mezclar los sonidos y enviarlos a la tarjeta de sonido. El buffer primario no lo vamos a utilizar normalmente; de hecho, si hemos establecido el nivel de cooperación DDSCL_NORMAL en la llamada a SetCooperativeLevel(), DirectSound nos permitirá acceder al buffer primario. Según la ayuda, el buffer primario puede ser utilizado para mezclar tú mismo los efectos, pero no lo he probado nunca.

Bueno, ya que no vamos a preocuparnos del buffer primario, vamos a crear los buffers secundarios, que serán los que contengan los efectos de sonido que vamos a utilizar. Para cada efecto que tengamos (un disparo, una puerta que se abre, etc) crearemos un buffer secundario.

Los buffers secundarios pueden ser de dos tipos, "static" o "streaming". Los buffers static contienen un sonido completo, a diferencia de los bufferes streaming, que sólo contienen una parte del sonido, y tu programa debe de encargarse de ir cargando en la memoria reservada para el buffer las distintas partes del sonido, antes de que DirectSound vaya a tocarlo. Por ejemplo, puedes tener una canción de 3 minutos y un buffer secundario de 10 segundos, y tu programa debe que DirectSound se vaya encontrando en esos 10 segundos de memoria el trozo de canción que debe tocar. Evidentemente, los buffers "streaming" ahorran memoria, pero para efectos de sonido cortos (disparos, pro ejemplo) no son necesarios.

Cuando creas un buffer secundario, DirectSound intentará primero reservar memoria de la tarjeta de sonido para guardar el sonido, y si no la encuentra lo guardará en la memoria del ordenador. Tocar los sonidos que estén en la memoria de la tarjeta de sonido es más rápido, por lo que debes procurar que aquellos sonidos que se vayan a utilizar más habitualmente durante el juego se carguen antes que los menos usados, para que así los sonidos más importantes estén almacenados en la memoria de la tarjeta de sonido.
El código para crear un buffer de sonido secundario será:


DirectSoundBuffer *SoundBuffer = NULL;
DSBUFFERDESC dsdesc;

dsdesc.dwSize = sizeof(dsdesc);
dsdesc.dwFlags = DSBCAPS_STATIC;

lpDirectSound->CreateSoundBuffer( &dsdesc, &SoundBuffer, NULL );

El tipo DSBUFFERDESC tiene un montón de campos para poner información, pero te remito a la ayuda de DirectX para consultarlos.

Bueno, nuestro siguiente paso es poner el buffer de sonido el efecto de sonido que queremos tocar. Si se trata de un efecto de sonido, lo normal será que lo tengamos en un fichero WAV. Lo que hay que hacer es leer el fichero WAV, y sacar de él los datos del sonido, y copiarlos en la memoria del buffer. Bueno, esto tiene más que ver con el formato WAV (que no conozco) que con DirectSound, por lo que no vamos a pararlos en ello. Pero eso no significa que no vayamos a leer ficheros WAV, si no vaya gracia...

Si te fijas en el directorio SDK/SAMPLES/MISC  del SDK de DirectX, encontrarás un fichero llamada DSUTIL.C. Este fichero tiene varias rutinas útiles para DirectSound, y entre ellas la función DSLoadSoundBuffer(), que se encarga de crear un buffer secundario y luego leer un fichero WAV que esté almacenado como recurso de nuestro programa y ponerlo en el buffer secundario. Por ejemplo:


LPDIRECTSOUNDBUFFER ChaingunBuffer = NULL;
ChaingunBuffer = DSLoadSoundBuffer( lpDirectSound,"CHAINGUN" );

Esta instrucción creará un buffer secundario llamado ChaingunBuffer, en el que la rutina almacenará el sonido que hemos definido como CHAINGUN en nuestra definición de recursos (el fichero .RC), de la siguiente forma:

CHAINGUN WAV chaingun.wav

Repito que esa rutina lee sonidos que están almacenados como recursos de nuestro programa (es decir, están dentro del ejecutable .exe), pero no creo que tengas ningún problema en cambiarla para que lea directamente ficheros .wav del disco, o incluso que extraiga un fichero .wav de un fichero de datos en el que incluyas todos los sonidos o imágenes del programa.

En el programa de ejemplo hay dos efectos de sonido, y los buffers se llaman ChaingunBuffer y DanceBuffer.

3. Tocar los sonidos

Una vez tenemos los sonidos almacenados en el buffer, ahora sólo tenemos que tocarlos. Es muy fácil:

DanceBuffer->Play(0,0,0);

Con sólo eso decimos a DirectSound que queremos que toque el sonido almacenado en DanceBuffer. En el tercer parámetro podemos decirle a DirectSound que queremos que cuando el sonido acabe vuelva a empezar (es decir, un loop), por ejemplo si queremos hacer una ametralladora:

ChaingunBuffer->Play(0,0,DSBPLAY_LOOPING);
Hay otras funciones como Stop(), SetVolume(), SetPanning(), etc. cuyo significado es bastante evidente, y no creo que necesiten mayor explicación.

4. Música de fondo

En esta parte sólo voy a tratar sobre cómo tocar un fichero MID de fondo. La cosa es bastante sencilla desde Windows, sólo hay que utilizar  las funciones que trae Windows para ello, en particular mciSendString(). Para hacer las cosas más fáciles, he cogido un fichero que venía con uno de los ejemplos de DirectX, que trae varias funciones para tocar un fichero mid, pararlo, hacer una pausa, etc. El fichero se llama midi.c y está bastante claro, o sea que no me voy a parar en ello.

Para tocar ficheros MOD ( o XM, S3M, etc), hay una librería llamada MIKMOD, que tiene una versión que utiliza DirectSound. No he probado la versión de DirectSound, pero la versión para DOS sí la he visto y es buena. Si quieres conseguirla, vete al site de siempre.

También hay otra librería muy conocida, llamada Midas, y creo que en sus últimas versiones soporta DirectSound, pero no sé nada concreto.

5. Antes de terminar: limpiar DirectSound:

Antes de terminar nuestra aplicación tenemos que liberar toda la memoria utilizada por DirectSound. Bueno, esto es bastante sencillo, sólo hay que llamar al método Release() de los objetos que hemos creado, tanto el objeto de DirectSound como los buffers secundarios. Todo esto lo hago en la función DirectSoundEnd():


void DirectSoundEnd( void )
 {
   bActive = FALSE;

   if( DanceBuffer) DanceBuffer->Release();
   if( ChaingunBuffer) ChaingunBuffer->Release();
   if( lpDirectSound) lpDirectSound->Release();
 }

Bueno, ya está. Por si no lo has cogido ya, aquí tienes otro link al programa de ejemplo para este mes. Lo único que hace es tocar un música de fondo y un par de sonidos cuando pulses los botones del ratón, pero espero que sea bastante didáctico. Como ejercicio, puedes intentar unir el programa del mes pasado (el comecocos que se movía) con el de este mes, y hacer un comecocos que se mueva mientras suena una música de fondo y de vez en cuando suene algún efecto.




AULA MACEDONIA
a
MACEDONIA Magazine