Thread Rating:
  • 0 Vote(s) - 0 Average
  • 1
  • 2
  • 3
  • 4
  • 5
TUTORIAL: Using OpenAL for 3D Sound
#1
[Image: fbopenal.png]

Introduction
I thought I should make a bit of an effort to show what OpenAL is capable of - I ported the headers yesterday and v1c's added it to the release. Big Grin

Download the Accompanying Tutorial Files

This tutorial will hopefully get you started using OpenAL in FreeBASIC. Why use OpenAL? Well if you're used to coding in OpenGL, you will find it extremely easy to switch. The function naming convention is the same and the co-ordinate system works the same way. The advantage of using OpenAL over a regular sound system is that it is 3D - that is, it will produce a directional sound effect according to your viewpoints position and direction relative to the sound source.

It even has capability of producing doppler effect! You probably notice the doppler effect in real life if your standing on a street and a car passes playing loud music. On the way towards you, the music sounds slightly faster and has a higher pitch. Once the car has passed you, the music will sound slower and has a slightly lower pitch.

Anyway, on with the tutorial!


[Image: sep.png]


Setting Up
The first thing you'll need is the OpenAL headers for FreeBASIC, if you haven't got them already with your FreeBASIC distribution. Check in your FreeBASIC 'inc' folder for a folder called 'al'. That should contain: al.bi, alc.bi, alctypes.bi, altypes.bi, alu.bi and alut.bi. If you need to download these headers, you can do so here:

Download OpenAL Headers

Extract the archive into your FreeBASIC folder and the headers and lib files should fall into the right locations. Smile

One little note... when you compile a program using OpenAL in FreeBASIC, ld will report this:

Warning: .drectve `/DEFAULTLIB:"uuid.lib" /DEFAULTLIB:"uuid.lib" ' unrecognized

After a lot of web searching, I have found out that apparently it is safe to ignore this warning. Big Grin


[Image: sep.png]


How OpenAL Works
Before starting the coding, I should explain how OpenAL works. There are 3 main things you need to be aware of: the listener, the buffers and the sources.

The Listener
The listener is simply a set of 3D co-ordinates and position information that describes the location of the listener (i.e. the player in your game). Each time you modify the position and orientation of the player, you should update the listener information.

The Buffers
Buffers are just audio data stored in memory. You have to "generate" some buffers to use (through a simple call) and then you can load WAV data into them. Each different sound file that you use should have its own buffer.

The Sources
A source is a 3D co-ordinate of where a sound should be coming from. All you need to describe the sound is a co-ordinate. Once a sound has been loaded into a buffer, you can 'bind' a buffer to a source. Sources have several different properties that you can modify - for example, the pitch.

A small note about custom data types... to make it easier for coders to change code from one language to another, the OpenAL developers have used their own definitions of data types. After including the FreeBASIC OpenAL headers, you can use any of the custom datatypes. Here is a list of the equivalences:
  • ALboolean - BYTE
    ALbyte - BYTE
    ALubyte - UBYTE
    ALshort - SHORT
    ALushort - USHORT
    ALint - INTEGER
    ALuint - UINTEGER
    ALfloat - SINGLE
    ALdouble - DOUBLE
    ALsizei - UNSIGNED LONG
    ALvoid - ANY
    ALenum - INTEGER

[Image: sep.png]


Initialising OpenAL
The first thing that we need to do is include the OpenAL header and the OpenAL utility library header. This can be done by placing these lines at the top of your code:

[syntax="FreeBASIC"]
'$INCLUDE: 'al/al.bi'
'$INCLUDE: 'al/alut.bi'
[/syntax]

Initialising OpenAL is really simple, just use the function alutInit, which takes 2 parameters. You can ignore these and just set them both to 0 (for anyone that's really interested, the parameters are argc and argv). At the end of our program, we must also make sure we call alutExit to unload the library.

Now our program looks like this:

[syntax="FreeBASIC"]
'$INCLUDE: 'al/al.bi'
'$INCLUDE: 'al/alut.bi'

alutInit(0, 0)

' Main code will go here!

alutExit
[/syntax]

The parenthesis around the subroutine calls aren't really needed, as you know, but I like to keep them there because it keeps the code tidy. Smile

The next stage is to set up the buffers, listener and a source! First I should explain a few things about the functions we'll be using. The main functions that we'll be calling to configure the different bits are very similar in name and work very similar. This is a list of functions relevant to the sources:
  • alSourcei(sourceid, param, value) - Sets an INTEGER option.
    alSourcef(sourceid, param, value) - Sets a SINGLE option.
    alSource3f(sourceid, param, v1, v2, v3) - Sets an option which accepts 3 SINGLEs.
    alSourcefv(sourceid, param, values) - Sets an option which accepts a variable number of SINGLEs.
The i, f, 3f and fv postfixes will stand out to OpenGL coders. Smile It is a useful way of showing exactly what type of parameter the function expects. The 'sourceid' is an ID identifying the source, the 'param' is used to tell OpenAL which option you want to set (e.g. you may want to set the position of a source) and the 'value' option tells OpenAL what to set these values to.

In the case of the last function (fv), the 'values' is a pointer to an array of values.

The functions for setting options for the listener follow exactly the same nomenclature: alListeneri, alListenerf, alListener3f and alListenerfv. There are no functions to set options for a buffer, because that's just not needed. Buffers only hold data, they don't interact in the 3D world by themselves.

We will need to generate all of the sources and buffers that we want to use. This is quite simple - first you need an array of buffers (an array of ALuint) and an array of sources (another array of ALuint). The parameters to the alGenBuffers and alGenSources are: number of buffers/sources to generate, pointer to the array of buffers/sources to store IDs in.

[syntax="FreeBASIC"]
DIM SHARED Buffer AS ALuint
DIM SHARED Source AS ALuint

alGenBuffers(1, @Buffer)
IF alGetError <> AL_NO_ERROR THEN
PRINT "Failed to generate buffers!"
alutExit
END
END IF

alGenSources(1, @Source)
IF alGetError <> AL_NO_ERROR THEN
' return the memory we just allocated for the buffer.
alDeleteBuffers(1, @Buffer)

PRINT "Failed to generate sources!"
alutExit
END
END IF
[/syntax]

I've decided to stick to using 1 buffer and source for this tutorial. Smile

You'll notice 2 other functions I haven't mentioned yet, but they're pretty obvious. alGetError returns the error code of the last error that occurred (NOTE: after returning the last error, alGetError resets the last error flag! So if you need to report the error after checking it, remember to save the value the function returned!). alDeleteBuffers is the opposite to alGenBuffers, but accepts the same parameters. As you can guess, alDeleteSources also exists for deallocating the sources. Smile

Actually, now we're mentioning all this, it would be nice to write a good little SUB to clean up after us.

[syntax="FreeBASIC"]
SUB UnloadOpenAL

' deallocate the buffers and sources.
alDeleteBuffers(1, @Buffer)
alDeleteSources(1, @Source)

' unload OpenAL gracefully
alutExit

END SUB
[/syntax]

Now we can call this before quitting. It will make sure that the resources we allocated are returned and that OpenAL exits safely.

Before setting up our 3D world, we need to load a sound. If you download the OpenAL header ZIP from the URL at the top of this document, there should be an example program bundled with it... I will be using the same WAV file as I used there - 'prodigy.wav'. This WAV file was taken from one of Nightbeat's amazing MODs. Big Grin

How do we go about loading the WAV file though? Well luckily the OpenAL utility library provides some functions to do the dirty work for us, mainly: alutLoadWAVFile and alutLoadWAVMemory. The first loads a WAV from a file and the second loads a WAV from a pointer, which is very useful if you'd prefer to put all of your WAV files into an archive, like a .PAK file... I wish lots of other API-coders would add this variation of their loading routines. Sad

Anyway... here is how it's done: you need to keep a few pieces of information loaded from the buffer. You'll need this information later on to bind a buffer to a source. This data includes format (describes the bit-rate, etc. of the sample), data (the raw wave data), size (the size of the wave data, in bytes), freq (the sample frequency) and loop (boolean value indicating whether the WAV file says this sample should loop). Don't worry about any of these values - OpenAL will store the information in your variables and read the information back out without any interference needed on your part.

Here is the code for this:

[syntax="FreeBASIC"]
' variables to store WAV data
DIM wavFormat AS ALenum
DIM wavSize AS ALsizei
DIM wavFreq AS ALsizei
DIM wavData AS ANY PTR
DIM wavLoop AS ALboolean

' load the WAV file
alutLoadWAVFile("prodigy.wav", @wavFormat, @wavData, @wavSize, @wavFreq, @wavLoop)

IF alGetError <> AL_NO_ERROR THEN
PRINT "Error: Failed to load WAV data!"
UnloadOpenAL
END
END IF
[/syntax]

It's no good just having a buffer - we need a source to glue it to. This makes it possible to hear the buffer's data in our 3D world. We can 'bind' the buffer to a source by using the alBufferData function. This function requests the buffer to apply the data to and all of the relevant information about the wave data. After binding the buffer to the source, we can return the memory occupied by the wave data by calling alutUnloadWAV.

After binding the buffer, we can set up the source - we tell it all of the information it needs to know to make the audio sound right. To do this, we use the alSource functions.

[syntax="FreeBASIC"]
alBufferData(Buffer, wavFormat, wavData, wavSize, wavFreq)
alutUnloadWAV(wavFormat, wavData, wavSize, wavFreq)

IF alGetError <> AL_NO_ERROR THEN
PRINT "Error: Failed to bind audio buffer to source!"
UnloadOpenAL
END
END IF

' set up the source audio info.
alSourcei(Source, AL_BUFFER, Buffer)
alSourcef(Source, AL_PITCH, 1.0)
alSourcef(Source, AL_GAIN, 1.0)
alSourcei(Source, AL_LOOPING, wavLoop)
[/syntax]

Cool, now let's review: we've initialised OpenAL, generated buffers and sources, loaded a WAV file and bound the audio data to a source!

The next stage is to set up the 3D information regarding buffer, source and listener. This is a list of the stuff we'll need to set up:
  • Listener position
    Listener velocity
    Listener orientation
    Source position
    Source velocity
For the sake of simplicity, we'll be keeping the velocity at 0. Also, I'm only going to be talking in 2 dimensions because through this tutorial, we will build up a mini walk-around environment... and I don't want to confuse everything by making the interface 3D! Just bear in mind that it is possible to use all 3 dimensions.

To configure all of these aspects of the listener and the source, we'll be using the functions that I talked about earlier (alSourcei, alSourcef, etc.).

[syntax="FreeBASIC"]
DIM ListenerPos(3) AS ALfloat
DIM ListenerVel(3) AS ALfloat
DIM ListenerOri(6) AS ALfloat
DIM SourcePos(3) AS ALfloat
DIM SourceVel(3) AS ALfloat

' Configure the listener
ListenerPos(0) = 0.0 ' x
ListenerPos(1) = 0.0 ' y
ListenerPos(2) = 0.0 ' z

ListenerVel(0) = 0.0 ' x
ListenerVel(1) = 0.0 ' y
ListenerVel(2) = 0.0 ' z

ListenerOri(0) = 0.0 ' at(x)
ListenerOri(1) = 0.0 ' at(y)
ListenerOri(2) = -1.0 ' at(z)
ListenerOri(3) = 0.0 ' up(x)
ListenerOri(4) = 0.0 ' up(y)
ListenerOri(5) = 1.0 ' up(z)

' set these values.
alListenerfv(AL_POSITION, @ListenerPos(0))
alListenerfv(AL_VELOCITY, @ListenerVel(0))
alListenerfv(AL_ORIENTATION, @ListenerOri(0))


' Configure the source
SourcePos(0) = 0.0 ' x
SourcePos(1) = 0.0 ' y
SourcePos(2) = 0.0 ' z

SourceVel(0) = 0.0 ' x
SourceVel(1) = 0.0 ' y
SourceVel(2) = 0.0 ' z

' set the physical values.
alSourcefv(Source, AL_POSITION, @SourcePos(0))
alSourcefv(Source, AL_VELOCITY, @SourceVel(0))
[/syntax]

Believe it or not, that is it! We've finished initialising everything. It is possible to modify the properties of the source and listener location later on in the program, by the way. That is how we're going to use the 3D sound effects (modifying the listener position).

If you're implementing this in a game, you will need to think of a more elegant method of coding this initialisation routine than I've presented here (obviously). I'll leave that up to you, however, as that's beyond the scope of this tutorial.

Just for fun, let's test out what we've done. Let's make some noise!...


[Image: sep.png]


Playing the Audio
All of our hard work initialising the sources, buffers and listener have finally paid off. It's easy-street for us now. Here are the functions you can use to control the audio of the sources in the 3D environment:
  • alSourcePlay
    alSourcePause
    alSourceStop
    alSourceRewind
The only argument that these functions take is the source that you want to apply the action to.

Okay, so now let's add a couple of lines to make a noise.

[syntax="FreeBASIC"]
alSourcePlay(Source)
WHILE INKEY$ = "": WEND
[/syntax]

How cool is that? If you're using a WAV file that doesn't loop, you can make it loop by changing the initialisation code. Scroll up in your code to this line:

[syntax="FreeBASIC"]
alSourcei(Source, AL_LOOPING, wavLoop)
[/syntax]

And just change it to this:

[syntax="FreeBASIC"]
alSourcei(Source, AL_LOOPING, AL_TRUE)
[/syntax]

Now recompile and run again - it should be looping now! Big Grin

How about trying this instead... just for fun... Wink

[syntax="FreeBASIC"]
alSourcePlay(Source)

pitch! = 1.0
done% = 0
WHILE done% = 0
a$ = INKEY$

IF a$ = "q" THEN

' decrease the pitch.
pitch! = pitch! - 0.1
IF pitch! < 0.1 THEN pitch! = 0.1

alSourcef(Source, AL_PITCH, pitch!)

ELSEIF a$ = "w" THEN

' increase the pitch
pitch! = pitch! + 0.1
IF pitch! > 4.0 THEN pitch! = 4.0

alSourcef(Source, AL_PITCH, pitch!)

ELSEIF a$ = " " THEN

done% = 1

END IF
WEND
[/syntax]

You can use 'q' to decrease the pitch, 'w' to increase the pitch and space to quit. Smile


[Image: sep.png]


Putting OpenAL to Work
We now have an amazingly powerful 3D sound library at our disposable. I am not going to venture too much further on from the point we've reached at the moment - things get a lot more complicated and are only really applicable to people using OpenGL in conjunction with OpenAL. After all, OpenAL is really designed to be used in 3D game development.

For this tutorial, I will make a small environment with the source at the centre. The source will be represented by a square and the player will be represented by a circle. You can move the player around using the WSAD keys.

Here is -all- of the code - the completed version of what this tutorial has built up to.

[syntax="FreeBASIC"]
'$INCLUDE: 'al/al.bi'
'$INCLUDE: 'al/alut.bi'

#define SC_SPACE &H39
#define SC_W &H11
#define SC_S &H1F
#define SC_A &H1E
#define SC_D &H20
#define SC_ESCAPE &H01

option explicit


'--- Function Declarations ---------------------------------------------------
declare sub UnloadOpenAL()


'--- Variable Declarations ---------------------------------------------------
dim shared Buffer as ALuint
dim shared Source as ALuint

' variables to store WAV data
DIM wavFormat AS ALenum
DIM wavSize AS ALsizei
DIM wavFreq AS ALsizei
DIM wavData AS ANY PTR
DIM wavLoop AS ALboolean

' position info.
DIM ListenerPos(3) AS ALfloat
DIM ListenerVel(3) AS ALfloat
DIM ListenerOri(6) AS ALfloat
DIM SourcePos(3) AS ALfloat
DIM SourceVel(3) AS ALfloat

' misc. variables
dim done as integer
dim lX as integer
dim lY as integer
dim sX as integer
dim sY as integer



'--- OpenAL Initialisation ---------------------------------------------------

alutInit(0, 0)


alGenBuffers(1, @Buffer)
IF alGetError <> AL_NO_ERROR THEN
PRINT "Failed to generate buffers!"
alutExit
END
END IF

alGenSources(1, @Source)
IF alGetError <> AL_NO_ERROR THEN
' return the memory we just allocated for the buffer.
alDeleteBuffers(1, @Buffer)

PRINT "Failed to generate sources!"
alutExit
END
END IF


' load the WAV file
alutLoadWAVFile("prodigy.wav", @wavFormat, @wavData, @wavSize, @wavFreq, @wavLoop)
IF alGetError <> AL_NO_ERROR THEN
PRINT "Error: Failed to load WAV data!"
UnloadOpenAL
END
END IF


alBufferData(Buffer, wavFormat, wavData, wavSize, wavFreq)
alutUnloadWAV(wavFormat, wavData, wavSize, wavFreq)

IF alGetError <> AL_NO_ERROR THEN
PRINT "Error: Failed to bind audio buffer to source!"
UnloadOpenAL
END
END IF

' set up the source audio info.
alSourcei(Source, AL_BUFFER, Buffer)
alSourcef(Source, AL_PITCH, 1.0)
alSourcef(Source, AL_GAIN, 1.0)
alSourcei(Source, AL_LOOPING, AL_TRUE)


' Configure the listener
ListenerPos(0) = 0.0 ' x
ListenerPos(1) = 0.0 ' y
ListenerPos(2) = 0.0 ' z

ListenerVel(0) = 0.0 ' x
ListenerVel(1) = 0.0 ' y
ListenerVel(2) = 0.0 ' z

ListenerOri(0) = 0.0 ' at(x)
ListenerOri(1) = 0.0 ' at(y)
ListenerOri(2) = -1.0 ' at(z)
ListenerOri(3) = 0.0 ' up(x)
ListenerOri(4) = 0.0 ' up(y)
ListenerOri(5) = 1.0 ' up(z)

' set these values.
alListenerfv(AL_POSITION, @ListenerPos(0))
alListenerfv(AL_VELOCITY, @ListenerVel(0))
alListenerfv(AL_ORIENTATION, @ListenerOri(0))


' Configure the source
SourcePos(0) = 0.0 ' x
SourcePos(1) = 0.0 ' y
SourcePos(2) = 0.0 ' z

SourceVel(0) = 0.0 ' x
SourceVel(1) = 0.0 ' y
SourceVel(2) = 0.0 ' z

' set the physical values.
alSourcefv(Source, AL_POSITION, @SourcePos(0))
alSourcefv(Source, AL_VELOCITY, @SourceVel(0))



'--- Main Game Loop ----------------------------------------------------------

' start playing the source.
alSourcePlay(Source)


screen 13


done = 0
while done = 0
cls

' draw the scene.
lX = int((ListenerPos(0) * 10) + 160)
lY = int((ListenerPos(2) * 10) + 100)
circle (lX, lY), 15, 7

sX = int((SourcePos(0) * 10) + 160)
sY = int((SourcePos(2) * 10) + 100)
line (sX - 5, sY - 5)-(sX + 5, sY + 5), 8, B

line (4, 6)-(314, 194), 15, B

flip


' check input, etc.
if multikey(SC_SPACE) or multikey(SC_ESCAPE) then

' Exit
done = 1

end if

if multikey(SC_A) then

' Left
ListenerPos(0) = ListenerPos(0) - 0.1
if ListenerPos(0) < -13.9 then ListenerPos(0) = -13.9


' update.
alListenerfv(AL_POSITION, @ListenerPos(0))

end if

if multikey(SC_D) then

' Right
ListenerPos(0) = ListenerPos(0) + 0.1
if ListenerPos(0) > 13.9 then ListenerPos(0) = 13.9

' update.
alListenerfv(AL_POSITION, @ListenerPos(0))

end if

if multikey(SC_W) then

' Up
ListenerPos(2) = ListenerPos(2) - 0.1
if ListenerPos(2) < -8.0 then ListenerPos(2) = -8.0

' update.
alListenerfv(AL_POSITION, @ListenerPos(0))

end if

if multikey(SC_S) then

' Down
ListenerPos(2) = ListenerPos(2) + 0.1
if ListenerPos(2) > 8.0 then ListenerPos(2) = 8.0

' update.
alListenerfv(AL_POSITION, @ListenerPos(0))

end if

' ~100 fps
sleep 10
wend


UnloadOpenAL
end


''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''
' UnloadOpenAL
'
sub UnloadOpenAL()

' deallocate the buffers and sources.
alDeleteBuffers(1, @Buffer)
alDeleteSources(1, @Source)

' unload OpenAL gracefully
alutExit
end sub
[/syntax]


[Image: sep.png]


More Help
The OpenAL header port is very young, so there are bound to be places where I copied something incorrectly, or dozed off while I was typing. Smile If you find any problems (e.g. incorrect function declarations), please e-mail me and I'll try to fix them as soon as possible.

You can also contact me if you're having troubles trying to get anything in this tutorial working... I will try to help, but I can't gaurantee anything!

Reach me by e-mail: c.g.davies@gmail.com
Or through my site: http://www.cdsoft.co.uk/

Thanks for reading - I hope this has been useful to you! If you make anything using stuff from this tutorial, please e-mail me as well... it's amazing to hear from people that have made use of the tutorials I write. Big Grin

-shiftLynx
img]http://www.cdsoft.co.uk/misc/shiftlynx.png[/img]
Reply
#2
good work!
url]http://fbide.sourceforge.net/[/url]
Reply
#3
This is very good. Thanks! I'm definetly going to use it... alot! Big Grin
Reply
#4
Yup, this is some nice stuff =)

Really good work Big Grin
Reply
#5
It was really easy to understand -- you type too fast :P

The default lib thing is added by VisualC, there's no way to remove it, unless compiling with other compiler, i will check the openal's cvs and try to rebuild alut using mingw..
Reply
#6
Nice!!!!!
y smiley is 24 bit.
[Image: anya2.jpg]

Genso's Junkyard:
http://rel.betterwebber.com/
Reply
#7
Docs!!!

Someoine put it in the safe box! Big Grin
Antoni
Reply
#8
openAL rox, great tutorial!!!
url=http://www.random-seed.net][Image: asylumsig.png][/url]
Reply
#9
Thanks everyone, it's great to hear all of this feedback. Smile
Dr_Davenstein: maybe you could use OpenAL in your OpenGL platform game... Smile 3D sound would be amazing.

Actually, thinking about it, it is perfectly possible to make a good large 3D game now. Dr_Davenstein did an amazing display of using OpenGL in FreeBASIC. There are music playing libs to play game music, 3D sound for realistic sound effects, 3D graphics capabilities... Maybe it would be possible to port Quake to FreeBASIC actually. Big Grin It's written in C, so there is no real reason why it shouldn't be possible.

-shiftLynx
img]http://www.cdsoft.co.uk/misc/shiftlynx.png[/img]
Reply
#10
Does OpenUL support MIDIs or mp3s?
f you play a Microsoft CD backwards you can hear demonic voices. The scary part is that if you play it forwards it installs Windows.
Reply


Forum Jump:


Users browsing this thread: 1 Guest(s)