DOS Coding: Sound Blaster sound


BASE_PORT = the base I/O port the SB is listening on (most often 0x220, but see 'detecting the card' below)

Single vs Auto mode:
For single transfer playback, you don't really need the Interrupt Service Routine.
I've shifted to auto-initialise mode because I was getting clicks between each transfer.
For auto-initialise mode, you set up the DMA for the whole buffer, but tell the SB/DSP it's half the buffer size.  That way, once the first half is transferred, you get an interrupt to say you can overwrite the first half.  Then when the whole lot is transferred, you get another interrupt to say you can overwrite the second half.  (By that time the DMA has already auto-reset back and started transferring the first half of the buffer)

Step 1: setting up sb_read() and sb_write() subroutines

You should really check if the soundblaster is ready before reading or writing.

Seeing as you need to write and read to detect/reset the card, these really come first.

Read: read from port BASE_PORT+0xE (status port) and check bit 7.  If it is set, go ahead and read from port BASE_PORT+0xA to get your byte.  If it is not set, try reading the status port again (with timeout).

Write: read from port BASE_PORT+0xC and check bit 7.  If it is set, go ahead and write.  If it is not set, read again.

Caveat: I couldn't get DosBox-X to ever set bit 7 for the write check, but it worked if I just went ahead and wrote the data anyway.  Go figure.

Step 2: detecting the card

You need to choose a BASE_PORT and perform a soundblaster reset on it.  If the reset works, it's a soundblaster.

There are articles elsewhere on parsing the BLASTER environment variable to get a port (e.g. this one).  You can also spam the different ports to see if it works.

I found this video really useful (including the common ports soundblasters listen on).

Important gotcha: THE PORT NUMBER IS IN HEX.  Parsing it in decimal and wondering why the hell it wasn't working cost me hours.

Performing a reset: write 1 to BASE_PORT+0x6, wait 3 us, then write 0 to BASE_PORT+0x6.  Then try to read a byte (as in step 1), and it should be 0xAA if it worked.

Step 3: setting up the DMA

This took me a while to figure out because I knew nothing about x86 DMA.  I originally thought it was part of the soundblaster itself!

The BLASTER var should give you the DMA channel, but apparently it is normally 1.

I found this video useful.
  1. Mask the DMA channel while you're monkeying with the values.
  2. Set the start address: reset the 'flip-flop', then write low-byte, then high-byte.
  3. Set the count (no. of bytes in 8-bit DMA): reset the 'flip-flop', then write low-byte, then high-byte. 
  4. Set the page register to the buffer's segment nibble.
  5. Set the DMA mode (single mode, no auto-reset, peripheral is reading, channel number)
  6. Unmask the DMA channel
Important gotcha: the segment of your audio buffer might also affect the offset.  For example, my buffer's offset was 0x29b2, and its segment was 0x1408.  This game a 20-bit address of 0x16A32.  This means for the DMA, I needed page 0x1 and start address 0x6A32 - just using FP_OFF() WILL NOT work.

Calculating offset for DMA: FP_OFF(buffer) + (FP_SEG(buffer)<<4);
Calculating page for DMA: ( (FP_OFF(buffer)>>4) + FP_SEG(buffer) ) >> 12;

Important gotcha 2: for auto-initialise mode, the DMA buffer length is THE WHOLE BUFFER.  Only the SB/DSP buffer length is half the buffer.

Step 4: Setting the SB for DMA

  1. Turn on the speaker: send command 0xD1
  2. Set the time constant: command 0x40, then send JUST THE HIGH BYTE - or set the sample rate directly if your DSP supports it.
  3. Set buffer length (actual value you write is buffer length - 1): command 0x48, low-byte then high-byte.
  4. Start transfer: command 0x14 (for single mode 8-bit unsigned), or 0x1C (for auto-initialise mode 8-bit unsigned)

Step 5: Setting up the Interrupt Service Routine

  1. get and store the old interrupt vector with _dos_getvect(irq+8)
  2. set your new interrupt with _dos_setvect(irq+8, interruptHandler)
  3. ISR should:
    1. check it's the correct interrupt (i.e. 8-bit DMA in my case)
    2. read from BASE_PORT+0xE to acknowledge interrupt with DSP
    3. outp(0x20, 0x20) to signal end-of-interrupt with PIC
    4. mix in the next part of the sound buffer

Code snippets for Watcom here.




Comments

Popular posts from this blog

Micro:Bit and SPI display

DCS World with TrackIR under Ubuntu

Cardboard Mock-up of USB Joystick