Sound cards which are Sound Blaster-compatible (i.e. which support the specifications that were present in the original, actual Sound Blaster spec) are capable of playing raw sound effects directly, without need to set up an IRQ or DMA channel. This was not usually done in games, since it meant that the game couldn't do very much besides pay attention to the sound effects, which isn't usually useful in a game which has to be doing lots of other things besides playing sound, but if you really just want to play anything with your sound card, and don't care about doing anything else at the same time, this sort of works. For more information on direct sound with the Sound Blaster, and why you might (or might not) want to use it, see The Two Ways You Can Play Digitized Sound On A Sound Blaster.
But this article is not about why; this article is about how. This article assumes you already know that you want to play direct sound on a Sound Blaster, and are simply interested in finding out how you can do so.
There is a command available on the Sound Blaster for direct DAC (Digital-to-Analog Conversion) which takes a digital sound waveform and converts it into an analog sound. What does digital sound look like? It simply is a string of data values which represent the level of a sound waveform at each sample point. Digital sound is made up of a series of samples, and each sample is just a number representing what level the sound waveform is at during that point in time. So, for example, a square wave could be represented as the following sequence of bytes:
00000000 00000000 00000000 11111111 11111111 11111111
Each low point and high point of this square wave is 3 bytes long. You can speed up the square wave by reducing each point to only 1 or 2 bytes, or you can slow it down by adding more bytes to each portion.
Before we get started on actually writing a program that'll play raw sounds, we might want to think for a moment about where we're going to get our sound data from, and how we'll represent it in a form the program can read. What we need is raw sound data; as it turns out, a regular WAV file, similar to the type used by more recent versions of Windows (versions after Windows 3.0), is *almost* a raw sound file; a WAV file has a small header that contains some basic data about the file, but after the header it's almost pure raw sound data. You could quite conceivably just record a WAV file, strip off the header, and end up with a perfectly good raw sound file. If you're going to do this, save the file as a PCM-format, 8-bit mono file. You could, also, just type up a long string of bytes to represent sound data and insert it into your program code as a literal data string, but for the purposes of this program, let's assume that you already have a raw sound in a filename called SOUNDATA.RAW, and that we're going to open this file with our program to retrieve the sound data.
Since we're going to use a file for our data, the first thing we do with our program should probably be to open this file and put it into a memory buffer. We can do this with lines of code similar to the following:
MOV AH,3Dh ;open file using handle MOV AL,0 ;read-only MOV DS,SEG filename MOV DX,OFFSET filename INT 21h ;AX should now be the file handle! MOV BX,AX ;transfer file handle to BX where it belongs MOV AH,3Fh ;read file using handle MOV CX,8000h ;number of bytes to be read MOV DS,SEG databuff MOV DX,OFFSET databuff INT 21h ;databuff should now hold the first 8000h bytes of the file! FILENAME DB 'soundata.raw', 00 databuff DB 8000h DUP(?)
The first block of code opens a file using INT 21,3D. We've set the function to look for a filename in the data location named "FILENAME", where we've stored the ASCIIZ (i.e. ASCII zero-terminated) value "soundata.raw". When this function is done running, AX should contain a handle which points to our newly-opened file.
The second block of code uses INT 21,3F to read data from our now-open file, and transfer 8000h (32,768 decimal) bytes from this file into a data buffer we've created in memory which is simply called "databuff". Note that this figure of 32,768 bytes is arbitrary; adjust this to the actual size of the sound file you're using. (Actually, it won't hurt anything if you make databuff too large; it'll just mean your program will occupy more memory than it needs. But don't let the file-reading operation go too far, or it might go clear past the end of the file and start reading bytes that don't belong to the sound file! In other words, make sure CX isn't too large when you call INT 21,3F.)
The final two lines form the actual raw data locations in the program. Note that these two lines should go at the very end of your program; otherwise, the program will run those data bytes as instructions, which isn't good! Those bytes are data, not opcodes!
Now let's turn on the speakers. There's actually a command to do this on the Sound Blaster, and it's sort of important that we use it, since otherwise the speakers will stay off and we won't be able to hear anything. The hexadecimal value of this command is 0D1h.
Before we send any commands to the Sound Blaster, though, we want to do two things. First, we want to establish the base address of the Sound Blaster. This is almost always 0220h, since that was the factory-default value and hardly anybody ever changed it, but if you have a different value for your SB, then by all means, change the next line as appropriate:
BASEADDR EQU 0220h
The second thing we want to do is establish a subroutine for waiting after sending a command to the Sound Blaster. The Sound Blaster, like almost any other electronic device, doesn't react to input instantly, and it may take a few cycles for it to do what you ask of it. For this reason, there's a mechanism to determine when the Sound Blaster has finished doing whatever you told it to do, and is ready to receive new commands. The procedure goes like this:
1. Send the command to the Sound Blaster by outputting a command byte to
BASEADDR+0Ch.
2. Before sending any additional commands, READ from BASEADDR+0Ch. (This I/O
port behaves completely differently depending on whether you read from or
write to it.)
3. If the data from the read returns a 0, the Sound Blaster is done with
whatever you told it to do, and it's ready to receive new commands. If you
got something other than a 0, return to step 2.
Since we need to do this after sending every single command to the Sound Blaster, it makes sense to make a nice little subroutine for this. I tend to call this subroutine WAITWRITE.
WAITWRITE: ;Waits for Sound Blaster to be ready before sending it more data MOV DX,BASEADDR+0Ch loopwait: in al,dx or al,al js loopwait RET
Once this function is inserted into your program, you can call it at any time just by using the line call WAITWRITE. Do this after every command you send to the Sound Blaster to help keep your Sound Blaster working.
Once you're sure that the Sound Blaster is ready to accept a new command, you can simply send it a command on port BASEADDR+0Ch. The command for direct DAC is 10h, and this byte must be sent for EACH raw sound byte you want to send. So basically, the process for playing a raw sound from this point forward contains 4 simple steps:
1. Send 10h to BASEADDR+0Ch.
2. Call WAITWRITE.
3. Send a raw sound byte to BASEADDR+0Ch.
4. Call WAITWRITE.
Not too bad, right? Here's a full assembler program that you can compile and run. All you need now is a raw sound file, like this one. (Actually, that's an 8,000 hertz, 8-bit, mono, PCM-format WAV file, but the WAV file's header at the beginning of the file is so small that you hardly even notice the brief blip of sound it makes before the actual sound plays.).
Note that if you just run this program directly, chances are your computer will play the sound *way* too fast for it to be at all recognizable. You'll need to slow down the rate at which the sound samples play. This can be done by either inserting some kind of delay loop into the program, or (probably an easier way if you just want to hear the sound) running the program in DOSBox and cranking the cycles in DOSBox to a very low setting. Note that, predictably, the higher the sampling frequency of a raw sound file, the more cycles per second a computer needs to be running at to play it at the correct speed.
BASEADDR EQU 0220h ;Sound Blaster base I/O address ;First, let's open our file: MOV AH,3Dh ;open file using handle MOV AL,0 ;read-only MOV DS,SEG filename MOV DX,OFFSET filename INT 21h ;AX should now be the file handle! MOV BX,AX ;transfer file handle to BX where it belongs MOV AH,3Fh ;read file using handle MOV CX,8000h ;number of bytes to be read MOV DS,SEG databuff MOV DX,OFFSET databuff INT 21h ;databuff should now hold the first 8000h bytes of the file! CALL WAITWRITE ;This call to WAITWRITE has already initialized DX to the right place to ;send commands, so we can freely start sending commands there once WAITWRITE ;returns. MOV AL,0D1h ;Turn speakers on OUT DX,AL CALL WAITWRITE ;Speakers are now on!!! MOV SI,OFFSET databuff ;Point SI to the sound data MOV CX,08000h ;number of bytes to play playloop: MOV AL,10h ;Direct play... Must be done for each byte! OUT DX,AL CALL WAITWRITE MOV AX,[SI] ;Actual sound data value INC SI ;Increment SI for the next sound data byte we play OUT DX,AX ;This plays the sound! CALL WAITWRITE loopnz playloop ;This keeps going through playloop for CX times mov ah,004C ;terminate program int 21h WAITWRITE: ;Waits for Sound Blaster to be ready before sending it more data MOV DX,BASEADDR+0Ch loopwait: in al,dx or al,al js loopwait RET FILENAME DB 'soundata.raw', 00 databuff DB 8000h DUP(?)