[ / main / writing / gus ]

    The Gravis Ultrasound



©1998 Jeff Weeks and Codex software

Sample source code:
gus.zip




Well, let's get right into it. Before ever using the GUS code, you must, ofcouse, check to see that a GUS exists. This is actually really easy. You can communicate to the GUS directly through its base port, so all your must do is write to every possible base port, and if what you read back is the same as what you wrote, you've not only detected a GUS card, but you've found its base port too!




Well, before you can do any of that, you'll need peek and poke (read and write) functions that operate using the GUS's ports. Here's what they look like:

    // read from the GUS's memory
    char gpeek(long loc) {
      short addlo;
      char  addhi;
      addlo = loc & 0xFFFF;
      addhi = long(loc & 0xFF0000) >> 16;
      outportb(port + 0x103, 0x43);
      outportw(port + 0x104, addlo);
      outportb(port + 0x103, 0x44);
      outportb(port + 0x105, addhi);
      return inportb(port + 0x107);
    }
    // write to the GUS's memory
    void gpoke(long loc, char stuff) {
      short addlo;
      char  addhi;
      addlo = loc & 0xFFFF;
      addhi = long(loc & 0xFF0000) >> 16;
      outportb(port + 0x103, 0x43);
      outportw(port + 0x104, addlo);
      outportb(port + 0x103, 0x44);
      outportb(port + 0x105, addhi);
      outportb(port + 0x107, stuff);
      return;
    }



Perhaps that looks a little complicated, but it really isn't, especially if you've ever looked at some Sound Blaster code ;) So, what's the above all about? Well, first of all, the GUS uses 24-bit memory addressing so the first two lines of almost all functions will convert the 32-bit address (loc) into a 24-bit address. That shouldn't be anything new, you basically do the same thing to convert a protected mode pointer to a real mode pointer.




So, next you have to read or write to this address. This is done by sending 0x43, or 0x44 to port 0x103, which tells the GUS what part of the address you're about to send it (as far as I know, anyway) and then you have to send that part of the address. It seems that the GUS combines port 0x104 and 0x105 to make the 24-bit address, so you will write the low half to 0x104, and the high half to 0x105. You can see that happening in the above code snippets.




Lastly, you can read and write from the data port, 0x107. Simple, right? So, let's use the above functions to create a detect function.




    // check to see if the GUS is at a certain base port
    char probe(int base) {
      char c;
      port = base;                      // needed for the peek and poke routines
      outportb(base + 0x103, 0x4C);
      outportb(base + 0x105, 0);
      gdelay();
      gdelay();
      outportb(base + 0x103, 0x4C);
      outportb(base + 0x105, 1);
      gdelay();
      gdelay();
      gpoke(0,0xF);
      gpoke(0x100,0x55);
      c = gpeek(0);
      if(c == 0xF) return 1;
      else return 0;
    }
    // find the GUS
    char find(void) {
      short i;
      short base;
      for(i = 1; i <= 8; i++) {
        base = 0x200 + i * 0x10;
        if(probe(base)) {
          printf("Found your GUS at base %x\n", base);
          break;
        }
        else printf("No GUS found at base %x\n", base);
      }
      return 0;
    }



The find function is probably pretty easy to understand, but the probe function probably needs some explanation. Unfortunately, I don't think I can give you the explanation you want. Basically all it does is set the global 'port' variable to the port in question. This is because the peek and poke functions require the port variable to do their work. The probe function does some initialization at the start, which I'm afraid I'm not totally sure what it does. You'll have to be content in knowing that it's required. Then it simply writes to the GUS, and reads back from the same address and compares the two. If they're the same, you've found a GUS.




Oh yeah, you'll notice the occasional interleaved gdelay() call. That's just a little delay to accomodate the GUS's processing. It's a simple function that just halts for about 7 cycles.




    // a delay, equivalent to approximately 7 clock ticks (actually 8)
    void gdelay(void) {
    #if defined __DJGPP__
      __asm__ (
             "movw $0x300, %dx\n"
             "inw  %dx, %al\n"
             "inw  %dx, %al\n"
             "inw  %dx, %al\n"
             "inw  %dx, %al\n"
             "inw  %dx, %al\n"
             "inw  %dx, %al\n"
             "inw  %dx, %al\n"
      );
    #else
      asm {
             mov dx, 0x300
             in  al, dx
             in  al, dx
             in  al, dx
             in  al, dx
             in  al, dx
             in  al, dx
             in  al, dx
      }
    #endif
      return;
    }



So, what else can we find out about our GUS? How about how much memory it has? That's very similar to detecting the GUS actually. You just write to the different possible memory boundaries (256k, 512k, 768k or 1024k) and read back from the same address. If you don't read the same value that you wrote, then you've passed the memory boundary for the GUS card.




    int gmemory(void) {
      int i;
      char b;
      gpoke(257*1024, 0xF);
      if(gpeek(257*1024) != 0xF) i = 256*1024;
      else {
        gpoke(513*1024, 0xF);
        if(gpeek(513*1024) != 0xF) i = 512*1024;
        else {
          gpoke(769*1024, 0xF);
          if(gpeek(769*1024) != 0xF) i = 768*1024;
          else i = 1024*1024;
        }
      }
      memsize = i;
      return i;
    }



Simple enough, right? I don't think that requires any explanation. So, let's actually create some sounds. You can create sounds on the GUS simply by writting to its memory and setting one of the voices to point to it. Oh, but wait, there is one last thing. You must now initialize the GUS. Here's what that looks like...




    void reset(void) {
      outportb(port+0x103, 0x4C);
      outportb(port+0x105, 1);
      delay();
      outportb(port+0x103, 0x4C);
      outportb(port+0x105, 7);
      outportb(port+0x103, 0x0E);
      outportb(port+0x105, 14 | 0x0C0);
      return;
    }



So, now you can use your gpoke routine to write your samples to anywhere withen the GUS's memory boundaries. With that done you have to initialize a voice and point it to your samples memory address. Initialization is done by setting up the volume, panning, and frequency.




    void volume(char channel, int vol) {
      outportb(port+0x102, channel);
      outportb(port+0x102, channel);
      outportb(port+0x102, channel);
      outportb(port+0x103, 9);
      outportw(port+0x104, vol);
      return;
    }
    void balance(char channel, char bal) {
      outportb(port+0x102, channel);
      outportb(port+0x102, channel);
      outportb(port+0x102, channel);
      outportb(port+0x103, 0xC);
      outportb(port+0x104, bal);
      return;
    }
    void frequency(char channel, int freq) {
      outportb(port+0x102, channel);
      outportb(port+0x102, channel);
      outportb(port+0x102, channel);
      outportb(port+0x103, 1);
      outportw(port+0x104, freq);
      return;
    }



You can see the technique is very similar for each of the above. Next, you set the voice pointers. Oddly enough you must specify a beginning, starting and ending address. The first two seem rather redundant, but they are needed. You'd usually just set them to the same thing.




    void playvoice(char chan, char mode, long begin, long start, long end) {
      outportb(port+0x102, chan);
      outportb(port+0x102, chan);
      outportb(port+0x103, 0x0A);
      outportw(port+0x104, (begin >> 7) & 8191);
      outportb(port+0x103, 0x0B);
      outportw(port+0x104, (begin & 0x127) << 8);
      outportb(port+0x103, 0x02);
      outportw(port+0x104, (start >> 7) & 8191);
      outportb(port+0x103, 0x03);
      outportw(port+0x104, (start & 0x127) << 8);
      outportb(port+0x103, 0x04);
      outportw(port+0x104, (end >> 7) & 8191);
      outportb(port+0x103, 0x05);
      outportw(port+0x104, (end & 0x127) << 8);
      outportb(port+0x103, 0x0);
      outportb(port+0x105, mode);
      outportb(port, 1);
      outportb(port+0x103, 0x4C);
      outportb(port+0x105, 3);
      return;
    }



That's a nice long one, and again you're going to have to take it on faith. The process is pretty simple though; You write to the command port, 0x104, and then write the data to the other appropriate ports. You'll notice I also provide a mode parameter. The GUS uses this byte to set up some special effects and voice parameters. The effects are as follows...




Bit Meaning
7 Wavetable IRQ pending. If IRQ's are enabled and looping is not enabled, an IRQ will be constantly generated until the voice is stopped. This means that you may get more than 1 IRQ if it isn't handled properly.
6 A value of 1 represents a decreasing address, while a value of 0 indicates an increasing address. This is a self-modifying variable as it might change directions when looping is enabled, and the sample hits a loop boundary.
5 When set, this will enable wavetable IRQs. These are generated when the voice hits the end address. These will be honored even if looping is enabled.
4 When set, bi-directional looping is enabled.
3 When set, the GUS will loop to the beginning address when the end address is reached.
2 A value of 1 represents 16-bit wave data, while a value of 0 represents 8-bit wave data.
1 When set, it tells the GUS to manually force the voice to stop.
0 The voice stopped status bit. If it is set, this voice is no longer playing.



Lastly, maybe you want to get some input from the GUS. For example, you can get the position of any voice through the following function...

    int voicepos(char channel) {
      long p;
      short temp0, temp1;
      outportb(port+0x102, channel);
      outportb(port+0x102, channel);
      outportb(port+0x102, channel);
      outportb(port+0x103, 0x8A);
      temp0 = inportw(port+0x104);
      outportb(port+0x103, 0x8B);
      temp1 = inportw(port+0x104);
      return (temp0 << 7) + (temp1 >> 8);
    }



Well, that's it. I hope you can now see why programmers love the GUS. It's a wonderful card with lots of hardware support.