/* SBIO.C  */
/* Soundblaster 16 basic audio I/O functions */
/* Copyright 1995 by Ethan Brodsky.  All rights reserved */
/* Modified extensively by Philip VanBaren to suit my purposes */
/* Ethan Brodsky's original SB16 sampling code may be found at: */
/*    ftp://oak.oakland.edu/simtel/msdos/sound/sb16snd.zip */

/* Interface variables that can be changed in the background */
volatile int  sb16dmarunning;
volatile char curblock;

#include <alloc.h>
#include <stdio.h>
#include <dos.h>
#include <mem.h>
#include <stdlib.h>
#include "sbio.h"
#include "freq.h"  /* Needed for DOUT() definition only */

#define lo(value) (unsigned char)((value) & 0x00FF)
#define hi(value) (unsigned char)((value) >> 8)

int  mixerport;
int  mixdataport;
int  resetport;
int  readport;
int  writeport;
int  pollport;
int  poll16port;

int  pic_rotateport;
int  pic_maskport;

int  dma_maskport;
int  dma_clrptrport;
int  dma_modeport;
int  dma_baseaddrport;
int  dma_countport;
int  dma_pageport;

char irq_startmask;
char irq_stopmask;
char irq_intvector;
char int_controller;

char dma_startmask;
char dma_stopmask;
char dma_mode;

/* This function is defined in sc_sb16.c */
extern void interrupt sb16_callback(void);
void interrupt (*oldintvector)() = NULL;
int  handlerinstalled;

void far *dmabuffer = NULL; /* Twice the size of the output buffer */
int far *dmaptr = NULL;     /* Pointer to the used portion */

unsigned long buf_addr;    /* 16-bit addressing */
unsigned char buf_page;
unsigned int  buf_ofs;

mode iomode;               /* Flags input or output mode */

/* Low level sound card I/O  */
void write_dsp(unsigned char value)
{
  while (inp(writeport) & 0x80);   /* Wait for bit 7 to be cleared */
  outp(writeport, value);
}

unsigned char read_dsp(void)
{
  long timeout=1000000L;
  /* Wait for bit 7 to be set, or for a timeout */
  while((!(inp(pollport) & 0x80)) && (--timeout));
  return inp(readport);
}

int reset_dsp(void)
{
  int i;

  sb16dmarunning=0;
  outp(resetport, 1);
  outp(resetport, 0);
  i = 100;

  while ((read_dsp() != 0xAA) && i--);
  return i;
}

/* Convert a far pointer to a linear address, for DMA addressing */
#define getlinearaddr(p) ((unsigned long)FP_SEG(p)*16 + (unsigned long)FP_OFF(p))

/* Initialization and shutdown  */
void installhandler(void)
{
  /* Install the interrupt handler */
  disable();                                     /* Disable interrupts  */
  outp(pic_maskport, (inp(pic_maskport)|irq_stopmask));  /* Mask IRQ    */
  oldintvector = getvect(irq_intvector);         /* Save old vector     */
  setvect(irq_intvector, sb16_callback);         /* Install new handler */
  outp(pic_maskport, (inp(pic_maskport)&irq_startmask)); /* Unmask IRQ  */
  enable();                                      /* Reenable interupts  */
  handlerinstalled = 1;
}
void uninstallhandler(void)
{
  sb16dmarunning=0;
  disable();                                     /* Disable interrupts  */
  outp(pic_maskport, (inp(pic_maskport)|irq_stopmask)); /* Mask IRQ     */
  setvect(irq_intvector, oldintvector);          /* Restore old vector  */
  enable();                                      /* Enable interrupts   */
  handlerinstalled = 0;
}
/* This function is run at program exit to ensure cleanup */
void sb_exitproc(void)
{
  sb16dmarunning=0;
  outp(0x20, 0x20);  outp(0xA0, 0x20);  /* Acknowledge any hanging ints */
  write_dsp(0xD5);                      /* Stop digitized sound xfer    */
  outp(dma_maskport, dma_stopmask);     /* Mask DMA channel             */
  if (handlerinstalled) uninstallhandler(); /* Uninstall int handler    */
  reset_dsp();                          /* Reset SB DSP                 */
}

/*
* Initialize the Soundblaster-16 card with the settings:
* baseio: base IO address for the card
*    irq: IRQ channel used by the card
*  dma16: 16-bit DMA channel used by the card
*     io: sampling mode, either "input" or "output"
* length: maximum block size to be used, in samples
*         (this is used for allocating the DMA buffer)
*/
int init_sb(int baseio, char irq, char dma16, mode io, unsigned int length)
{
  int val,onboard;

  /* Sound card IO ports */
  mixerport  = baseio + 0x004;
  mixdataport = baseio + 0x005;
  resetport  = baseio + 0x006;
  readport   = baseio + 0x00A;
  writeport  = baseio + 0x00C;
  pollport   = baseio + 0x00E;
  poll16port = baseio + 0x00F;

  /* Reset DSP */
  sb16dmarunning=0;
  if (!reset_dsp()) return 0;

  /* Verify that we have a SB-16 at this address */
  write_dsp(0xE1);
  val=read_dsp();    /* Get the major version number */
  read_dsp();        /* Grab the minor version number also */
  if(val<4) return 0;

  /* Check the current IRQ and DMA settings, and compare with
   * the values passed to this routine */
  outportb(mixerport,0x80);   /* IRQ settings */
  val=inportb(mixdataport);
  onboard=-1;
  if(val&0x01) { onboard=2;  DOUT("SB16: IRQ2 enabled"); }
  if(val&0x02) { onboard=5;  DOUT("SB16: IRQ5 enabled"); }
  if(val&0x04) { onboard=7;  DOUT("SB16: IRQ7 enabled"); }
  if(val&0x08) { onboard=10; DOUT("SB16: IRQ10 enabled"); }
  if(onboard==-1)
  {
     puts("Error: Soundblaster has no IRQ enabled, aborting ...");
     return(0);
  }
  switch(irq)
  {
     case 2:  if(!(val&0x01)) { irq=-1; } break;
     case 5:  if(!(val&0x02)) { irq=-1; } break;
     case 7:  if(!(val&0x04)) { irq=-1; } break;
     case 10: if(!(val&0x08)) { irq=-1; } break;
     default: irq=-1;
  }
  if(irq==-1)
  {
     printf("Warning: Soundblaster has IRQ%d selected, using that value instead\n",onboard);
     irq=onboard;
  }
  outportb(mixerport,0x81);   /* DMA settings */
  val=inportb(mixdataport);
  onboard=-1;
  if(val&0x01) { /*onboard=0;*/ DOUT("SB16: DMA0 enabled"); }
  if(val&0x02) { /*onboard=1;*/ DOUT("SB16: DMA1 enabled"); }
  if(val&0x08) { /*onboard=3;*/ DOUT("SB16: DMA3 enabled"); }
  if(val&0x20) { onboard=5; DOUT("SB16: DMA5 enabled"); }
  if(val&0x40) { onboard=6; DOUT("SB16: DMA6 enabled"); }
  if(val&0x80) { onboard=7; DOUT("SB16: DMA7 enabled"); }
  if(onboard==-1)
  {
     puts("Error: Soundblaster has no 16-bit DMA enabled, aborting ...");
     return(0);
  }
  switch(dma16)
  {
//      case 0:  if(!(val&0x01)) { dma16=-1; } break;
//      case 1:  if(!(val&0x02)) { dma16=-1; } break;
//      case 3:  if(!(val&0x08)) { dma16=-1; } break;
     case 5:  if(!(val&0x20)) { dma16=-1; } break;
     case 6:  if(!(val&0x40)) { dma16=-1; } break;
     case 7:  if(!(val&0x80)) { dma16=-1; } break;
     default: dma16=-1;
  }
  if(dma16==-1)
  {
     printf("Warning: Soundblaster has DMA%d selected, using that value instead\n",onboard);
     dma16=onboard;
  }

  #ifdef DEBUG_OUTPUT
     /* Check if the IRQs are enabled */
     outportb(mixerport,0x82);   /* IRQ status */
     val=inportb(mixdataport);
     if(val&0x01) { DOUT("SB16: 8-bit IRQ active"); }
     if(val&0x02) { DOUT("SB16: 16-bit IRQ active"); }
     if(val&0x04) { DOUT("SB16: MPU-401 IRQ active"); }
  #endif
  /* These two lines are strictly a kludge for freq.exe */
  outportb(mixerport,0x3d);    /* Input control */
  outportb(mixdataport,0x7f);  /* Use all channels for input */

  /* Compute interrupt ports and parameters */
  if (irq < 8)
  {
     int_controller = 1;
     pic_rotateport = 0x20;
     pic_maskport   = 0x21;
     irq_intvector  = 0x08 + irq;
  }
  else
  {
     int_controller = 2;
     pic_rotateport = 0xA0;
     pic_maskport   = 0x21;
     irq_intvector  = 0x70 + irq-8;
  }
  irq_stopmask  = 1 << (irq % 8);
  irq_startmask = ~irq_stopmask;

  /* Compute DMA ports and parameters */
  dma_maskport     = 0xD4;
  dma_clrptrport   = 0xD8;
  dma_modeport     = 0xD6;
  dma_baseaddrport = 0xC0 + 4*(dma16-4);
  dma_countport    = 0xC2 + 4*(dma16-4);

  switch(dma16)
  {
     case 5:  dma_pageport = 0x8B; break;
     case 6:  dma_pageport = 0x89; break;
     case 7:  dma_pageport = 0x8A; break;
  }

  dma_stopmask  = dma16-4 + 0x04;    /* 000001xx */
  dma_startmask = dma16-4 + 0x00;    /* 000000xx */

  /* Allocate a buffer for DMA transfer */
  /* (need a block of memory that does not cross a page boundary) */
  if ((dmabuffer = farmalloc(8*length)) == NULL)
  {
     puts("Unable to allocate DMA buffer.");
     exit(1);
  }
  dmaptr = (int far *)dmabuffer;
  if(((getlinearaddr(dmabuffer) >> 1) % 65536L) + length*2 > 65536L)
     dmaptr += 2*length; /* Pick second half to avoid crossing boundary */

  /* Compute DMA parameters */
  buf_addr = getlinearaddr(dmaptr);
  buf_page = (unsigned int)(buf_addr >> 16);
  buf_ofs  = (unsigned int)((buf_addr >> 1) % 65536L);

  /* Other initialization */
  iomode = io;
  switch (iomode)
  {
     case input:  dma_mode = dma16-4 + 0x54; break;  /* 010101xx */
     case output: dma_mode = dma16-4 + 0x58; break;  /* 010110xx */
  }

  installhandler();    /* Install interrupt handler */
  atexit(sb_exitproc); /* Install exit procedure    */

  return 1;
}
/*
* Shut down the sampling process, clean up the interrupts,
* and free up the memory allocation
*/
void shutdown_sb(void)
{
  sb16dmarunning=0;
  reset_dsp();
  if(handlerinstalled) uninstallhandler();
  farfree(dmabuffer);
}

/*
* Start continuous I/O at a rate of {rate} Hz, with a call to the
* callback_sb16 procedure after every block of length {length} has
* been finished.  The sampling input or output is done on alternating
* blocks pointed to by (dmaptr+curblock*length).
*  i.e. When curblock is 0, the current block is (dmaptr).
*       When curblock is 1, the current block is (dmaptr+length)
* The variable curblock is maintained in the callback_sb16 routine.
*/
void startio(unsigned int rate, unsigned long length)
{
  sb16dmarunning = 1;
  curblock = 0;

  /* Program DMA controller */
  outp(dma_maskport,     dma_stopmask);
  outp(dma_clrptrport,   0x00);
  outp(dma_modeport,     dma_mode);
  outp(dma_baseaddrport, lo(buf_ofs));           /* Low byte of offset  */
  outp(dma_baseaddrport, hi(buf_ofs));           /* High word of offset */
  outp(dma_countport,    lo(length*2-1));        /* Low byte of count   */
  outp(dma_countport,    hi(length*2-1));        /* High byte of count  */
  outp(dma_pageport,     buf_page);
  outp(dma_maskport,     dma_startmask);

  /* Program sound card */
  switch (iomode)
  {
     case input:  write_dsp(0x42); break;  /* Set input sampling rate  */
     case output: write_dsp(0x41); break;  /* Set output sampling rate */
  }
  write_dsp(hi(rate));                     /* High byte of sampling rate */
  write_dsp(lo(rate));                     /* Low byte of sampling rate  */
  switch (iomode)
  {
     case output: write_dsp(0xB6); break;  /* 16-bit D->A, A/I, FIFO   */
     case input:  write_dsp(0xBE); break;  /* 16-bit A->D, A/I, FIFO   */
  }
  write_dsp(0x10);                     /* DMA Mode:  16-bit signed mono */
  write_dsp(lo(length-1));             /* Low byte of block length      */
  write_dsp(hi(length-1));             /* High byte of block length     */
}
/*
* Stops the current sampling process.
*/
void stopio(void)
{
  outp(0x20, 0x20);  outp(0xA0, 0x20);  /* Acknowledge any hanging ints */
  write_dsp(0xD9);                      /* Stop digitized sound xfer    */
  outp(dma_maskport, dma_stopmask);     /* Mask DMA channel             */
  sb16dmarunning=0;
  reset_dsp();                          /* Reset SB DSP                 */
}

/* Mixer setting and reading functions */
void set_cd_level(unsigned int level)
{
  level=level*256/100;
  if(level>255) level=255;
  outportb(mixerport,0x36); /* CD Audio left */
  outportb(mixdataport,level);
  outportb(mixerport,0x37); /* CD Audio right */
  outportb(mixdataport,level);
}

void set_mic_level(unsigned int level)
{
  level=level*256/100;
  if(level>255) level=255;
  outportb(mixerport,0x3A); /* Microphone */
  outportb(mixdataport,level);
}

void set_line_level(unsigned int level)
{
  level=level*256/100;
  if(level>255) level=255;
  outportb(mixerport,0x38); /* Line In left */
  outportb(mixdataport,level);
  outportb(mixerport,0x39); /* Line In right */
  outportb(mixdataport,level);
}

unsigned int get_cd_level(void)
{
  unsigned int level;
  outportb(mixerport,0x36); /* CD Audio left */
  level=inportb(mixdataport);
  outportb(mixerport,0x37); /* CD Audio right */
  level+=inportb(mixdataport);
  level=level*50/256;
  if(level>100) level=100;
  return(level);
}

unsigned int get_mic_level(void)
{
  unsigned int level;
  outportb(mixerport,0x3A); /* Microphone */
  level=inportb(mixdataport);
  level=level*100/256;
  if(level>100) level=100;
  return(level);
}

unsigned int get_line_level(void)
{
  unsigned int level;
  outportb(mixerport,0x38); /* Line In left */
  level=inportb(mixdataport);
  outportb(mixerport,0x39); /* Line In right */
  level+=inportb(mixdataport);
  level=level*50/256;
  if(level>100) level=100;
  return(level);
}