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:
-
DDSCL_EXCLUSIVE
-
DDSCL_PRIORITY
-
DDSCL_NORMAL
-
DDSCL_WRITEPRIMARY
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.