In this document, we will look at using a SoundBlaster compatible card with QBasic. Such a card actually combines different methods or systems for producing sound. These methods may include waveform sound, FM systhesizer music, sound from the Creative Music System (not used a lot nowadays), MIDI compatibility and wave table sounds. Also, many sound cards are capable of digitizing sound. In this document, we will limit ourselves to discussing waveform and FM sound, because these two methods are the most widespread. With the experience gained from working with these methods and the specifications of the system, you will be able to work out how to control other sound systems.
Controlling a sound card in QBasic is done by sending data to hardware
ports, by means of the OUT
statement. Hardware ports are the connections
between the computer and peripheral devices, such as printers or modems.
Data can also be send the other way, from the device to the computer. In
QBasic, this data is read in using the INP
function.
The SoundBlaster card can be configured to use different sets of hardware ports, identified by their base address, which is the number of the first port in the set. For instance, if the card is set to use hardware port numbers 220h to 233h, then its base address is 220h. To control the sound card, you have to find out its base address. Usually, the install program that comes with the sound card, has set up an environment variable containing the base address. If you type the command
SETat the DOS command line, a list will be printed with all the environment variables. There should be an item in the list that looks like this:
BLASTER=A220 I5 D1 T3The number just after the 'A' is the base address, in hexadecimal notation, in this case 220h. The other numbers specify the interrupt request number, the DMA channel number and the version number, but these don't concern us. The programs in this document assume 220h to be the base address, so if your card is set to another address, you will have to change the value BaseAddr in the programs.
To save the programs discussed in this document to your hard disk in standard ASCII format, click on the name with the right-hand mouse button and select 'Save Link As...' from the menu.
There are two ways of sampling sound: through the processor or through DMA (Direct Memory Access). DMA means that the sound data goes directly into the computer's memory, without intervention of the processor. DMA is faster, but fairly difficult, if not impossible, to accomplish in QBasic. Here, we will look at retrieving one byte of sound data at a time through the processor.
To let the sound card take a sample of the sound, we have to send the right command number to the command port. For DSP (Digital Signal Processing, what we are doing now), the number of the command port is base+Ch. Command numbers include 10h, which means 'Output a value to the speakers' and 20h, meaning 'Read a value from the microphone'. This value can then be read from the data port, which has number base+Ah. There are many other command numbers, but these can be found in more specific literature. To see how to use command 20h, we will have a look at program 1, which plots the sampled data on the screen.
SAMPLE.BAS
CONST ScreenMode = 12, xMax = 640 'Change for other screen modes CONST BaseAddr = &H220 'Change if your sound card uses another base address CONST CommAddr = BaseAddr + &HC, DataAddr = BaseAddr + &HA DEFINT A-Z DIM Byte(xMax) SCREEN ScreenMode DO OUT CommAddr, &H20 'Give command to sample a byte PRESET (i, Byte(i)) Byte(i) = INP(DataAddr) 'Read value from data port PSET (i, Byte(i)) i = (i + 1) MOD xMax 'Wrap i when end is reached LOOP
xMax
is the maximum x-coordinate,
which may have to change as well. An array is defined to hold the value for
each x-coordinate on the screen. In the loop that follows, the command 20h
is send to the command port. Then, the sample value is read in from the data
port and assigned to an element of the array Byte()
. This value is then
plotted on the screen. The PRESET
statement clears the points of the
previous plot. The variable i is incremented each time until it reaches
xMax, and then it is set back to zero. Figure 1 shows what you may see when
you run SAMPLE.BAS
and talk into the microphone.
SAMPLE.BAS
RECPLAY.BAS
is an implementation of this concept.
RECPLAY.BAS
DECLARE SUB ResetSB () DECLARE SUB Record () DECLARE SUB PlayBack () CONST NoOfSamples = 32766 'Maximum array length CONST BaseAddr = &H220 'Change if your sound card uses another base address CONST CommAddr = BaseAddr + &HC, DataAddr = BaseAddr + &HA CONST ResetAddr = BaseAddr + &H6 DEFINT A-Z DIM SHARED Byte(NoOfSamples) DO CLS PRINT "1. Record sound" PRINT "2. Play back sound" PRINT "3. Quit" DO Choice$ = INPUT$(1) LOOP WHILE INSTR("123", Choice$) = 0 'Check for valid choice SELECT CASE Choice$ CASE "1" Record CASE "2" PlayBack CASE "3" CLS END END SELECT LOOP SUB Record CLS PRINT "Recording..." LOCATE 3, 1 PRINT STRING$(NoOfSamples / 500, "±"); 'Print bar LOCATE 3, 1 ResetSB time! = TIMER FOR i = 0 TO NoOfSamples IF i MOD 500 = 0 THEN PRINT "Û"; 'Fill up bar OUT CommAddr, &H20 'Give command to sample a byte Byte(i) = INP(DataAddr) 'Read value from data port NEXT i time! = TIMER - time! LOCATE 5, 1 PRINT "Sampling rate:"; NoOfSamples / time!; "Hz." PRINT "Press any key to continue." key$ = INPUT$(1) END SUB SUB PlayBack CLS PRINT "Playing back..." LOCATE 3, 1 PRINT STRING$(NoOfSamples / 500, "±"); 'Print bar LOCATE 3, 1 ResetSB OUT CommAddr, &HD1 'Turn speaker on time! = TIMER FOR i = 0 TO NoOfSamples IF i MOD 500 = 0 THEN PRINT "Û"; 'Fill up bar OUT CommAddr, &H10 'Give command to output a byte OUT CommAddr, Byte(i) 'Output value NEXT i time! = TIMER - time! OUT CommAddr, &HD3 'Turn speaker off LOCATE 5, 1 PRINT "Play back rate:"; NoOfSamples / time!; "Hz." PRINT "Press any key to continue." key$ = INPUT$(1) END SUB SUB ResetSB OUT ResetAddr, 1 OUT ResetAddr, 0 END SUB
SUB ResetSB
. In the main module
of the program, an array Byte()
is defined with 32767 elements, which is the
largest array QBasic can handle. Then, a menu is printed with three choices.
The first choice leads to the SUB Record
. In this SUB
, an empty bar is
printed, which is filled up with one block for each 500 samples taken. This
indicates the progress of the recording. After resetting the DSP chip, the
array Byte()
is filled with sound samples in the same way as in program 1.
When the array is full, the sampling rate is calculated by deviding the
number of samples taken by the time it took to take these samples. The
sampling rate is a measure for the quality of the recorded sound. After
waiting for a keypress, the program returns to the menu.
The second choice starts the SUB PlayBack
, which is very similar to the
SUB
Record
. After resetting the DSP chip, the speaker is switched on by sending
the command number D1h to the command port. Then the command to output a
byte to the speaker (10h) is send to the command port, followed by the byte
in question. After all bytes are sent, the speaker is switched off again, by
means of the command D3h.
When you run this program, you can record a few seconds of sound, and play it back again. If you happen to own a very fast computer, the record time may be very short. In that case, you could insert some kind of delay loop in the program, having samples taken less often. Of course, increasing the record time brings down the sampling rate (since the number of samples is fixed) and thus the sound quality.
Also, when running this program on a very fast computer, you may run into
problems. The DSP chip needs time to process the commands sent to it. When
command are sent too short after one another, things may go wrong. To avoid
this, you have to check if the DSP chip is ready to receive a new command.
This can be done by reading a byte from the command port. If bit 7 of this
byte is clear, i.e. zero, the DSP chip is ready to receive a command. So if
you run into problems using RECPLAY.BAS
, insert the following lines before
each OUT statement that writes to the command port:
DO LOOP WHILE (INP(CommAddr) AND 128) = 0This pauses the program for as long as the DSP chip is processing other commands. On slower computers, this loop isn't necessary, and would only bring down the sampling rate.
RECPLAY.BAS
by adding possibilities for saving the
recorded sound to disk and loading other sounds. You also might want to play
sounds recorded with another program, for instance in the WAV format. This
is the format used by Microsoft Windows to store sampled sounds in. It
contains a header of 44 bytes, followed by the wave data. In this header,
bytes 25-28 and bytes 29-32 both contain the sampling rate of the sound.
Bytes 41-44 contain the number of sound samples. The following program
WAVE.BAS
plays an 8 bit mono WAV file. The program is quite simple, not
taking into account the original sampling rate. Because of QBasic array
size limits, it can only play the first 32766 bytes of the file. However,
the program serves as a good example for playing sound from a file.
WAVE.BAS
DECLARE SUB ResetSB () DECLARE SUB PlayWav (FileName$) CONST BaseAddr = &H220 'Change if your sound card uses another base address CONST CommAddr = BaseAddr + &HC, ResetAddr = BaseAddr + &H6 DEFINT A-Z LINE INPUT "Enter file name: "; FileName$ PlayWav FileName$ END SUB PlayWav (FileName$) PRINT "Loading file..." OPEN FileName$ FOR BINARY AS #1 dummy$ = INPUT$(40, #1) 'Discard first 40 bytes length& = CVL(INPUT$(4, #1)) 'Next 4 bytes is length (4 bytes = LONG) IF length& > 32766 THEN 'Only WAVs shorter than 32767 bytes can be played PRINT "Lenght of file exceeds maximum array length." PRINT "Only the first 32766 bytes will be played." length& = 32766 END IF length = length& 'Convert to integer for more speed DIM Byte(1 TO length) FOR i = 1 TO length Byte(i) = ASC(INPUT$(1, #1)) 'Read a byte in NEXT i CLOSE #1 PRINT "Playing back..." ResetSB OUT CommAddr, &HD1 'Turn speaker on FOR i = 1 TO length OUT CommAddr, &H10 'Give command to output a byte OUT CommAddr, Byte(i) 'Output value NEXT i OUT CommAddr, &HD3 'Turn speaker off END SUB SUB ResetSB OUT ResetAddr, 1 OUT ResetAddr, 0 END SUB
SUB PlayWav
. This subprogram is devided into two parts: the loading
and the playing of the file. The file is first loaded into an array for more
speed. Of the header, the first 40 bytes are discarded. We are only
interested in the length of the data, bytes 41-44. These four bytes are read
in as a string and converted to a variable of data type LONG
with the
CVL
function. If the length exceeds 32766 bytes, a message is printed and the
length is set to 32766. Then, the data is read in byte by byte into the
array. This array is then played in the same way as in RECPLAY.BAS
.
This program should give you an idea of how to play data from disk. You could add a delay loop in the program so that the play back rate corresponds to the sampling rate. You could also let the file be played back backwards, to discover those hidden messages on the new Beatles record.
To conclude this section, we will give an overview of the DSP command numbers we used. There are a lot more than we give here; these are mainly concerned with DMA DSP. Information about this can be found in more specific literature.
Table 1: Some DSP commands numbers
Number | Command | Remarks |
---|---|---|
10h | Direct DAC, 8 bit | Send byte directly after command |
20h | Direct ADC, 8 bit | Sampled byte can be read from port address base+Ah |
1Dh | Enable speaker | |
3Dh | Disable speaker |
FM stands for frequency modulation. The sound is formed by having a carrier sound being modulated by a modulator sound. We can define up to nine 'instruments', each consisting of a carrier and a modulator. We can let these nine instruments play different notes together, producing complicated tunes. The instruments are defined by a lot of parameters, from which we will discuss only a few.
The FM chip on the sound card is programmed by setting registers in the chip to certain values. There are 224 of such registers, so you'll understand that we won't discuss every one of those. To set a register to a value, we send the number of the register to the Register Port, whose address is base+8. Then, we send the desired value to the Data Port, with address base+9. The carrier and modulator of each instrument both have four registers in which parameters are placed. Since there are nine channels (instruments), that already gives us 2 x 4 x 9 = 72 registers to program! Of course, we don't have to use all nine channels. The register numbers for the carrier of channel 1 are given in table 2.
Table 2: FM register numbers for carrier of channel 1
Number | Function |
---|---|
20h | Amplitude modulation/Vibrato/EG type/Key scaling/Octave shift |
40h | Key scaling level/Output level |
60h | Attack rate/Decay rate |
80h | Sustain level/Release time |
The functions given in table 2 will be explained below. To find the other 68 register numbers, add the offset numbers from table 3 to the base numbers in table 2.
Table 3: FM register offset numbers for carrier and modulator of channels 1-9
Channel | Offset for carrier | Offset for modulator |
---|---|---|
1 | 00h | 03h |
2 | 01h | 04h |
3 | 02h | 05h |
4 | 08h | 0Bh |
5 | 09h | 0Ch |
6 | 0Ah | 0Dh |
7 | 10h | 13h |
8 | 11h | 14h |
9 | 12h | 15h |
For example, to find the register number for the Attack rate/Sustain rate of the modulator of channel 6, add 0Dh to 60h to find 6Dh.
To define an instrument, values should be assigned to parameters. As you can see in table 2, two or more parameters are combined into one register. Each register is eight bits wide, so the register values can range from 0 to 255. These eight bits are devided over two or more parameters, so each parameter has less than eight bits available. For instance, if a parameter has three bits available, its values will range from 0 to 7. The total register value is found by combining the values for the different parameters using the appropiate coefficients.
We will now look at what the parameters mean. We will look at the registers for the carrier of channel 1, i.e. 20h, 40h, 60h and 80h, but the same goes of course for the other channels.
Register 20h looks like this:
Bit | 7 | 6 | 5 | 4 | 3 | 2 | 1 | 0 |
---|---|---|---|---|---|---|---|---|
Parameter | AM | Vib | Octave shift |
Bit | 7 | 6 | 5 | 4 | 3 | 2 | 1 | 0 |
---|---|---|---|---|---|---|---|---|
Parameter | AM | Vib |
Register 40h looks like this:
Bit | 7 | 6 | 5 | 4 | 3 | 2 | 1 | 0 |
---|---|---|---|---|---|---|---|---|
Parameter | Scaling | Output level |
Before we look at registers 60h and 80h, we first need to know a little bit more about how a note played on an instrument is built up. We distinguish four phases in the note. First, there is the 'attack'. This is the fast rise in level at the beginning of the note. Then follows the 'decay'. This is when, after reaching peak level, the sound volume drops till a certain level. This level is called the 'sustain' volume. The sound stays at this level until the 'release' time is reached, at which point the sound stops. Look at figure 2 to see a graphical representation of this idea.
Bit | 7 | 6 | 5 | 4 | 3 | 2 | 1 | 0 |
---|---|---|---|---|---|---|---|---|
Parameter | Attack rate | Decay rate |
The sustain level and release time are controlled by register 80h, which look like this:
Bit | 7 | 6 | 5 | 4 | 3 | 2 | 1 | 0 |
---|---|---|---|---|---|---|---|---|
Parameter | Sustain level | Release time |
As you can see, defining an instrument is not the simplest of tasks. There are still more registers, but I'm sure you've had enough for a while by now.
Now we will have a look at how to actually use the instrument we have just learnt to define. To hear a note play, we have to specify the note and the octave. We have eight octaves at our disposal, numbered 0-7. The notes, normally written down as letters, have gotten numbers. These can be found in table 4:
Table 4: FM note representations
Note | Number |
---|---|
C# | 16Bh |
D | 181h |
D# | 198h |
E | 1B0h |
F | 1CAh |
F# | 1E5h |
G | 202h |
G# | 220h |
A | 241h |
A# | 263h |
B | 287h |
C | 2AEh |
Bit | 7 | 6 | 5 | 4 | 3 | 2 | 1 | 0 |
---|---|---|---|---|---|---|---|---|
Parameter | Eight LSB of note number |
Bit | 7 | 6 | 5 | 4 | 3 | 2 | 1 | 0 |
---|---|---|---|---|---|---|---|---|
Parameter | Unused | Switch | Octave | Two MSB |
Now we're ready to play some music. Program 4 shows how to play a simple tune, using channels 1, 2 and 3.
FM-TUNE.BAS
DECLARE SUB SetReg (Reg%, Value%) CONST BaseAddr = &H220 'Change if your sound card uses another base address CONST RegAddr = BaseAddr + 8, DataAddr = BaseAddr + 9 DEFINT A-Z FOR i = 0 TO 224 SetReg i, 0 'Clear all registers NEXT i SetReg &H20, &H1 'Plays carrier note at specified octave ch. 1 SetReg &H23, &H1 'Plays modulator note at specified octave ch. 1 SetReg &H40, &H1F 'Set carrier total level to softest ch. 1 SetReg &H43, &H0 'Set modulator level to loudest ch. 1 SetReg &H60, &HE4 'Set carrier attack and decay ch. 1 SetReg &H63, &HE4 'Set modulator attack and decay ch. 1 SetReg &H80, &H9D 'Set carrier sustain and release ch. 1 SetReg &H83, &H9D 'Set modulator sustain and release ch. 1 SetReg &H21, &H1 'Plays carrier note at specified octave ch. 2 SetReg &H24, &H1 'Plays modulator note at specified octave ch. 2 SetReg &H41, &H1F 'Set carrier total level to softest ch. 2 SetReg &H44, &H0 'Set modulator level to loudest ch. 2 SetReg &H61, &HE4 'Set carrier attack and decay ch. 2 SetReg &H64, &HE4 'Set modulator attack and decay ch. 2 SetReg &H81, &H9D 'Set carrier sustain and release ch. 2 SetReg &H84, &H9D 'Set modulator sustain and release ch. 2 SetReg &H22, &H1 'Plays carrier note at specified octave ch. 3 SetReg &H25, &H1 'Plays modulator note at specified octave ch. 3 SetReg &H42, &H1F 'Set carrier total level to softest ch. 3 SetReg &H45, &H0 'Set modulator level to loudest ch. 3 SetReg &H62, &HE4 'Set carrier attack and decay ch. 3 SetReg &H65, &HE4 'Set modulator attack and decay ch. 3 SetReg &H82, &H9D 'Set carrier sustain and release ch. 3 SetReg &H85, &H9D 'Set modulator sustain and release ch. 3 READ NoOfNotes FOR i = 1 TO NoOfNotes time! = TIMER FOR j = 0 TO 2 'Voices 0, 1 and 2 READ octave READ note$ SELECT CASE note$ CASE "C#" SetReg &HA0 + j, &H6B 'Set note number SetReg &HB0 + j, &H21 + 4 * octave 'Set octave and turn on voice CASE "D" SetReg &HA0 + j, &H81 SetReg &HB0 + j, &H21 + 4 * octave CASE "D#" SetReg &HA0 + j, &H98 SetReg &HB0 + j, &H21 + 4 * octave CASE "E" SetReg &HA0 + j, &HB0 SetReg &HB0 + j, &H21 + 4 * octave CASE "F" SetReg &HA0 + j, &HCA SetReg &HB0 + j, &H21 + 4 * octave CASE "F#" SetReg &HA0 + j, &HE5 SetReg &HB0 + j, &H21 + 4 * octave CASE "G" SetReg &HA0 + j, &H2 SetReg &HB0 + j, &H22 + 4 * octave CASE "G#" SetReg &HA0 + j, &H20 SetReg &HB0 + j, &H22 + 4 * octave CASE "A" SetReg &HA0 + j, &H41 SetReg &HB0 + j, &H22 + 4 * octave CASE "A#" SetReg &HA0 + j, &H63 SetReg &HB0 + j, &H22 + 4 * octave CASE "B" SetReg &HA0 + j, &H87 SetReg &HB0 + j, &H22 + 4 * octave CASE "C" SetReg &HA0 + j, &HAE SetReg &HB0 + j, &H22 + 4 * octave END SELECT NEXT j READ duration! DO LOOP WHILE time! + duration! > TIMER 'Wait as long as duration FOR j = 0 TO 2 SetReg &HB0 + j, 0 'Switch voices off NEXT j NEXT i END DATA 15: REM Number of notes 'Data below: octave1, note1, octave2, note2, octave3, note3, duration DATA 4,B,4,G,4,D,.5 DATA 4,B,4,G,4,D,.5 DATA 4,B,4,G,4,D,.5 DATA 4,B,4,G,4,D,.5 DATA 5,D,4,B,4,F#,.25 DATA 4,C,4,A,4,E,.25 DATA 4,C,4,A,4,E,.25 DATA 4,B,4,G,4,D,.25 DATA 4,A,4,E,3,C,1 DATA 4,A,4,F#,4,D,.5 DATA 4,A,4,F#,4,D,.5 DATA 4,B,4,G,4,E,.5 DATA 4,C,4,A,4,F#,.5 DATA 5,D,4,A,4,F#,1 DATA 5,G,5,D,4,B,.5 SUB SetReg (Reg, Value) OUT RegAddr, Reg OUT DataAddr, Value END SUB
SUB SetReg
is declared. This SUB
puts the specified value into
the specified register. Then, all registers are cleared. The registers for
the first three channels are set to three identical instruments; some kind
of electronic piano sound. The octaves and notes are read from the
DATA
statements, and the SELECT CASE
statement chooses the correct number for the
note. The octave and note numbers are put in their respective registers, and
the note starts playing. We wait for a time specified by the duration
variable using the TIMER
system variable, and then registers B0h, B1h and
B2h are set to zero, and bit 5 with them, to switch the channel off. The
DATA
statements at the end of the program describe the tune. The first
DATA
statement specifies the number of notes, and the statements that follow
specify the octave and note for each channel, and the duration of the note
in seconds.
Working out the correct values for an instrument can be a long and tedious
process. However, there are ways of making this easier. For your convenience,
I have made the program FM-LAB.BAS
. This program
lets you play with four of the parameters: the attack rate, the decay rate,
the sustain level and the release time. When you run this program, a screen
is printed as depicted in figure 4.
FM-LAB.BAS
Enter
to hear the note
you have just defined. Esc
ends the program. This program demonstrates
very well the effects the different parameters have on the sound. You could
expand this program to include the other parameters as well. When you are
satisfied with the sound, you could use the values in a program similar to
FM-TUNE.BAS
.
The program FM-LAB.BAS
doesn't introduce new SoundBlaster programming
techniques, so we won't have a detailed look at it. We hope that this
document has given you some idea on SoundBlaster programming, and has
encouraged you to perform some experminents of your own.
OUT
and INP
statements. We have looked at digitizing sound
from an external sound source. We have seen that the sound can be plotted,
stored and played back. We have seen how we can read a WAV file and how to
play it back. We have looked at a number of FM registers for specifying
instruments, notes and octaves. We used this knowledge to program a simple
tune using three channels. Finally, we have experimented a bit with four of
the parameters: attack rate, decay rate, sustain level and release time.
Wouter Bergmann Tiest