Using a sound card in QBasic

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

SET

BLASTER=A220 I5 D1 T3

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

Byte()

PRESET

SAMPLE.BAS

SAMPLE.BAS

RECPLAY.BAS

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

Byte()

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) = 0

RECPLAY.BAS

WAVE.BAS

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

LONG

CVL

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

SUB

DATA

SELECT CASE

TIMER

DATA

DATA

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

Esc

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

INP