SET
BLASTER=A220 I5 D1 T3
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
First, some useful constants are defined. If your hardware
doesn't support screen mode 12, change it to another mode. 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.
Figure 1: The output of the program SAMPLE.BAS
With this program, you could use your PC as an oscilloscope. However, you can't measure voltages with it, because the SoundBlaster uses a technique called Automatic Gain Control (AGC), which automatically adjusts the recording level according to the level of the sound source. This means that (between certain boundaries) different levels of sound volume will produce the same level on the screen.
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
At the start of the program, a new constant is defined: the
reset port, base+6h. The DSP chip is reset by sending the value 1
to this port, followed by the value 0. This is done in the 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) = 0
This 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.
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
The main module only asks for the file name to play. Control
is then passed to the 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 |
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 |
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 |
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 |
Bit | 7 | 6 | 5 | 4 | 3 | 2 | 1 | 0 |
---|---|---|---|---|---|---|---|---|
Parameter | Scaling | Output level |
Figure 2: The different phases in a note
The attack rate and decay rate are specified by register 60h:
Bit | 7 | 6 | 5 | 4 | 3 | 2 | 1 | 0 |
---|---|---|---|---|---|---|---|---|
Parameter | Attack rate | Decay rate |
Bits 4-7 specify the attack rate, a value between 0 (slowest) and 15 (fastest).
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 |
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 |
while register B0h looks like this:
Bit | 7 | 6 | 5 | 4 | 3 | 2 | 1 | 0 |
---|---|---|---|---|---|---|---|---|
Parameter | Unused | Switch | Octave | Two MSB |
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
First, the 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.
Figure 4: The screen of the program FM-LAB.BAS
The parameter currently chosen is highlighted. You can adjust the
value for this parameter using the up and down arrow keys. You
can choose another parameter with the left and right arrow keys.
Press 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.