/*
 *  Copyright (c) 1994/95 by Jaroslav Kysela (Perex soft)
 *  Routines for control of CS4231 (CODEC) chip
 */
  
#include  "gus_dev.h"
#include  "pcm.h"

/* playback flags */

#define PFLG_NONE	0x0000
#define PFLG_USED	0x0001
#define PFLG_FLUSH	0x0002

/* record flags */

#define RFLG_NONE	0x0000
#define RFLG_USED	0x0001
#define RFLG_FLUSH	0x0002

/* other */

#define TIMER_VALUE	( 100000L / 40L )	/* 1/40 sec */

/*
 *  Some variables
 */

short codec_mode = CODEC_MODE_NONE;

static short codec_pflags = PFLG_NONE;
static short codec_rflags = RFLG_NONE;

static volatile short codec_play_blk = 0;
static volatile short codec_record_blk = 0;

static int codec_timer_value;
static int codec_playback_timer;
static int codec_playback_timer_new;
static int codec_record_timer;
static int codec_record_timer_new;

static struct codec_freq_stru codec_freq[14] = {
  {  5,  5510, 0x00 | CODEC_XTAL2 },
  {  6,  6620, 0x0E | CODEC_XTAL2 },
  {  8,  8000, 0x00 | CODEC_XTAL1 },
  {  9,  9600, 0x0E | CODEC_XTAL1 },
  { 11, 11025, 0x02 | CODEC_XTAL2 },
  { 16, 16000, 0x02 | CODEC_XTAL1 },
  { 18, 18900, 0x04 | CODEC_XTAL2 },
  { 22, 22050, 0x06 | CODEC_XTAL2 },
  { 27, 27042, 0x04 | CODEC_XTAL1 },
  { 32, 32000, 0x06 | CODEC_XTAL1 },
  { 33, 33075, 0x0C | CODEC_XTAL2 },
  { 37, 37800, 0x08 | CODEC_XTAL2 },
  { 44, 44100, 0x0A | CODEC_XTAL2 },
  { 48, 48000, 0x0C | CODEC_XTAL1 }
};

static struct codec_image_stru codec_image = {
  0x00,							/* lic */
  0x00,							/* ric */
  0x80,							/* la1ic */
  0x80,							/* ra1ic */
  0x80,							/* la2ic */
  0x80,							/* ra2ic */
  0x80,							/* loc */
  0x80,							/* roc */
  0x20,							/* pdfr */
  CODEC_AUTOCALIB,					/* ic */
  0x00,							/* pc */
  0x00,							/* ti */
  CODEC_MODE2,						/* mi */
  0x00,							/* lbc */
  0x00,							/* pbru */
  0x00,							/* pbrl */
  0x80,							/* afei */
  0x01,							/* afeii */
  0x00,							/* llic */
  0x00,							/* rlic */
  0x00,							/* tlb */
  0x00,							/* thb */
  0x00,							/* afs */
  0x00,							/* mioc */
  0x20,							/* cdfr */
  0x00,							/* cbru */
  0x00,							/* cbrl */
};

/*
 *  CODEC detection
 */
  
static short codec_version( void )
{
  short res;

  CLI();
  codec_out( CODEC_MISC_INFO, CODEC_MODE2 );
  MB();
  res = INB( CODECP( REG ) ) & 0x0f;
  STI();
  return res;
}

int codec_init( void )
{
  short i, j;
  short version;

  CLI();
  INB( CODECP( STATUS ) );		/* clear any pendings IRQ */
  OUTB( 0, CODECP( STATUS ) );
  MB();
  STI();
  
  for ( i = 0; i < 1000; i++ )
    {
      MB();
      if ( INB( CODECP( REGSEL ) ) & CODEC_INIT )
        for ( j = 0; j < 50; j++ ) gus_delay();
       else
        {
          version = codec_version();
          if ( version >= 1 && version < 15 ) break;
        }
    }
  
  version = codec_version();
  if ( version >= 1 && version < 15 )
    {
      codec_image.ic = 
        ( codec_image.ic & ~CODEC_SINGLE_DMA ) | 
        ( !max_flag && gus_dma1 == gus_dma2 ? CODEC_SINGLE_DMA : 0 );
      CLI();
      codec_out( CODEC_MCE | CODEC_IFACE_CTRL, codec_image.ic );
      OUTB( CODEC_IFACE_CTRL, CODECP( REGSEL ) );
      MB();
      STI();
      return version;
    }
  return 0;
}

/*
 *  CODEC I/O
 */

static void codec_auto_calibrate( void )
{
  if ( codec_image.ic & CODEC_AUTOCALIB )
    {
      CLI();
      codec_wait_reg_sel( CODEC_TEST_INIT );
      while ( INB( CODECP( REG ) ) & CODEC_CALIB_IN_PROGRESS )
        OUTB( CODEC_TEST_INIT, CODECP( REGSEL ) );
      STI();
    }
}

static void codec_wait_mce( unsigned char reg, unsigned char data )
{
  long timeout;

  CLI();
  codec_out( CODEC_MCE | reg, data );
  INB( CODECP( REG ) );
  INB( CODECP( REG ) );
  timeout = 100000;
  while ( ( INB( CODECP( REG ) ) & 0x80 ) && timeout-- > 0 ) ;
  if ( ( INB( CODECP( REG ) ) & 0x80 ) && !( pcm_mode & PCM_MODE_ADPCM ) )
    PRINTK( "codec_wait_mce: timeout\n" );
  codec_wait_reg_sel( reg );
  STI();
  codec_auto_calibrate();
}

static void codec_set_freq( void )		/* freq in Hz */
{
  short i;
  int freq;
  
  codec_image.pdfr &= 0xf0; freq = pcm_rate / 1000L; 
#if 0
  PRINTK( "pcm_rate: %d\n", freq );
#endif
  if ( freq > 48 ) freq = 48; 
  for ( i = 0; i < 14; i++ )
    if ( freq <= codec_freq[ i ].hertz )
      {
        codec_image.pdfr |= codec_freq[ i ].bits;
        break;
      }
#if 0
  PRINTK( "codec_set_freq: format=0x%x, i=%d\n", codec_image.pdfr, i );
#endif
  codec_wait_mce( CODEC_PLAYBK_FORMAT, codec_image.pdfr );
}

static unsigned char codec_get_format( void )
{
  unsigned char format;

  format = 0;
  if ( pcm_mode & PCM_MODE_16 )
    {
      if ( pcm_mode & PCM_MODE_ADPCM )
        format |= CODEC_ADPCM_16;
       else
      if ( pcm_mode & PCM_MODE_BIG )
        format |= CODEC_LINEAR_16_BIG;
       else
        format |= CODEC_LINEAR_16;
    }
   else
    {
      if ( pcm_mode & PCM_MODE_ALAW )
        format |= CODEC_ALAW_8;
       else
      if ( pcm_mode & PCM_MODE_ULAW )
        format |= CODEC_ULAW_8;
       else
        format |= CODEC_LINEAR_8;	/* I know, OR with 0 */
    }
  if ( pcm_mode & PCM_MODE_STEREO )
    format |= CODEC_STEREO;
#if 0
  PRINTK( "codec_get_format: 0x%x (pcm_mode=0x%x)\n", format, pcm_mode );
#endif
  return format;
}

static void codec_set_play_format( void )
{
  codec_image.pdfr &= 0x0f;
  codec_image.pdfr |= codec_get_format();
  codec_wait_mce( CODEC_PLAYBK_FORMAT, codec_image.pdfr );
}
 
static void codec_set_rec_format( void )
{
  codec_image.cdfr &= 0x0f;
  codec_image.cdfr |= codec_get_format();
  codec_wait_mce( CODEC_REC_FORMAT, codec_image.cdfr );
  if ( codec_image.ic & CODEC_SINGLE_DMA )
    codec_set_play_format();
}

static int codec_get_timer_value( short record )
{
  int value;
  
  value = pcm_rate;
  if ( pcm_mode & PCM_MODE_ADPCM ) value >>= 2;
  if ( pcm_mode & PCM_MODE_16 ) value <<= 1;
  if ( pcm_mode & PCM_MODE_STEREO ) value <<= 1;
#if 0
  PRINTK( "codec_get_timer_value: size = %d, rate = %d\n", (int)pcm_playback.size, (int)value );
#endif
  value = ( 10500L * ( record ? pcm_record.size : pcm_playback.size ) ) / value;
  value *= 10;
  if ( value < 0 )
    {
      PRINTK( "codec_get_timer_value: timer overflow!!! (%d)\n", (int)value );
      value = 0xffff;
    }
#if 0
  PRINTK( "codec_get_timer_value: value = %d\n", (int)value );
#endif
  return value;
}

static void codec_timer_start( unsigned short value )
{
  CLI();
  if ( value > 0xffff ) value = 0xffff;
  codec_image.afei |= CODEC_TIMER_ENABLE;
  codec_out( CODEC_TIMER_LOW, (unsigned char)value );
  codec_out( CODEC_TIMER_HIGH, (unsigned char)( value >> 8 ) );
  codec_out( CODEC_ALT_FEATURE_1, codec_image.afei );
  codec_timer_value = value;
  STI();
}

static void codec_timer_stop( void )
{
  CLI();
  codec_image.afei &= ~CODEC_TIMER_ENABLE;
  codec_out( CODEC_ALT_FEATURE_1, codec_image.afei );
  STI();
}
 
static void codec_timer_schedule( void )
{
  int ticks;

  if ( codec_image.afei & CODEC_TIMER_ENABLE )	/* timer enabled */
    {
      CLI();		/* codec_timer_stop = STI() */
      ticks = codec_timer_value - ( codec_in( CODEC_TIMER_LOW ) |
                                    ( codec_in( CODEC_TIMER_HIGH ) << 8 ) );
      codec_timer_stop();
#if 0
      PRINTK( "codec_timer_schedule: ticks = %d\n", ticks );
#endif
      if ( codec_playback_timer > 0 )
        codec_playback_timer -= ticks;
      if ( codec_record_timer > 0 )
        codec_record_timer -= ticks;
    }
  if ( codec_playback_timer_new > 0 )
    {
      codec_playback_timer = codec_playback_timer_new;
      codec_playback_timer_new = -1;
    }
  if ( codec_record_timer_new > 0 )
    {
      codec_record_timer = codec_record_timer_new;
      codec_record_timer_new = -1;
    }
  ticks = codec_playback_timer > 0 ?
            (
              ticks = codec_record_timer > 0 ? 
                (
                  codec_playback_timer < codec_record_timer ? 
                    codec_playback_timer : codec_record_timer
                ) : codec_playback_timer 
            ) : codec_record_timer;
#if 0
  PRINTK( "codec_timer_schedule: new timer = %d\n", ticks );
#endif            
  if ( ticks <= 0 )
    codec_timer_stop();
   else
    codec_timer_start( ticks );
}

void codec_open( void )
{
  CLI();
  codec_out( CODEC_ALT_FEATURE_1, codec_image.afei = 0x80 );
  codec_out( CODEC_ALT_FEATURE_2, codec_image.afeii = 0x01 );
  STI();
  codec_playback_timer = codec_playback_timer_new =
  codec_record_timer = codec_record_timer_new = -1;
  codec_set_freq();
  codec_set_rec_format();
  codec_set_play_format();
  /* ok. now enable CODEC IRQ */
  CLI();
  codec_out( CODEC_IRQ_STATUS, CODEC_PLAYBACK_IRQ | 
                               CODEC_RECORD_IRQ | 
                               CODEC_TIMER_IRQ );
  codec_out( CODEC_MCE | CODEC_IRQ_STATUS, 0 );
  OUTB( 0, CODECP( STATUS ) );		/* clear IRQ */
  codec_image.pc |= CODEC_IRQ_ENABLE;
  codec_out( CODEC_MCE | CODEC_PIN_CTRL, codec_image.pc );
  OUTB( 0, CODECP( REGSEL ) );		/* shut down MCE bit */
  STI();
  codec_mute_output = 1;
  codec_set_mute( CODEC_OUTPUT, 
                  current_mix[ MIX_PCM ].left_mute,
                  current_mix[ MIX_PCM ].right_mute );
}

void codec_close( void )
{
  codec_mute_output = 1;
  codec_set_mute( CODEC_OUTPUT, 
                  current_mix[ MIX_PCM ].left_mute,
                  current_mix[ MIX_PCM ].right_mute );
  CLI();
  /* now disable CODEC IRQ */
  codec_out( CODEC_IRQ_STATUS, 0 );
  OUTB( 0, CODECP( STATUS ) );		/* clear IRQ */
  codec_image.pc &= ~CODEC_IRQ_ENABLE;
  codec_out( CODEC_PIN_CTRL, codec_image.pc );
  /* disable timer */
  codec_timer_stop();
  /* disable both dma channels */
  disable_dma( gus_dma1 );
  if ( gus_dma2 != gus_dma1 ) disable_dma( gus_dma2 );
  /* now disable record & playback */
  codec_image.ic &= ~( CODEC_PLAYBACK_ENABLE | CODEC_RECORD_ENABLE );
  codec_out( CODEC_MCE | CODEC_IFACE_CTRL, codec_image.ic );
  OUTB( 0, CODECP( REGSEL ) );		/* shutdown MCE bit */
  STI();
}

/*
 *
 */

static void codec_switch_buffers( struct stru_pcm_buf *pcm_buf )
{
  unsigned int *i1, *i2;
  unsigned int tmp;
  unsigned int size;
  
#if 0
  PRINTK( "switch!!!!!!!\n" );
#endif
  i1 = (int *)pcm_buf -> buf[ 0 ];
  i2 = (int *)pcm_buf -> buf[ 1 ];
  if ( pcm_buf -> size & 3 )
    PRINTK( "codec_switch_buffers: Warning! unaligned size\n" );
  for ( size = pcm_buf -> size >> ( 1 + 2 ); size > 0; size--, i1++, i2++ )
    {
      tmp = *i1;
      *i1 = *i2;
      *i2 = tmp;
    }
}

/*
 *
 */
 
static void codec_start_playback( void )
{
  short active;
  int size;
  
  active = pcm_playback.active;
  if ( pcm_playback.used < 2 ) active ^= 1;
  if ( active ) 
    {
      codec_switch_buffers( &pcm_playback );
      pcm_playback.active ^= 1;
    }
  codec_play_blk = 0;
  size = pcm_playback.size << 1;
#if 0
  PRINTK( "codec_start_playback: start (size=%d)\n", size );
#endif
  CLI();
  disable_dma( gus_dma2 );
  if ( max_flag && gus_dma2 > 3 )
    {
      OUTB( max_cntrl_val & ~0x20, GUSP( MAXCNTRLPORT ) );
      OUTB( max_cntrl_val, GUSP( MAXCNTRLPORT ) );
    }
  clear_dma_ff( gus_dma2 );
  set_dma_mode( gus_dma2, DMA_MODE_WRITE | 0x10 );
  enable_dma( gus_dma2 );
  set_dma_addr( gus_dma2, (int)pcm_playback.buf[ 0 ] );
  set_dma_count( gus_dma2, size );
  if ( pcm_mode & PCM_MODE_ADPCM ) 
    size >>= 2; 
   else
    {
      if ( pcm_mode & PCM_MODE_16 ) size >>= 1;
      if ( pcm_mode & PCM_MODE_STEREO ) size >>= 1;
    }
  size--;
  codec_out( CODEC_PLY_LWR_CNT, (unsigned char)size );
  codec_out( CODEC_PLY_UPR_CNT, (unsigned char)(size >> 8) );
  codec_image.ic |= CODEC_PLAYBACK_ENABLE;
  codec_out( CODEC_IFACE_CTRL, codec_image.ic );	/* go! */
  codec_playback_timer = -1;
  codec_playback_timer_new = codec_get_timer_value( 0 );
  codec_timer_schedule();
  STI();
  codec_pflags = PFLG_USED;	/* timer IRQs are accepted from this point */
  codec_mute_output = 0;				/* enable output */
  codec_set_mute( CODEC_OUTPUT, 
                  current_mix[ MIX_PCM ].left_mute,
                  current_mix[ MIX_PCM ].right_mute );
#if 0
  PRINTK( "residue0: %d\n", get_dma_residue( gus_dma2 ) );
#endif
}

static void codec_stop_playback( void )
{
  codec_mute_output = 1;
  codec_set_mute( CODEC_OUTPUT, 
                  current_mix[ MIX_PCM ].left_mute,
                  current_mix[ MIX_PCM ].right_mute );
  codec_playback_timer = codec_playback_timer_new = -1;
  codec_timer_schedule();
  if ( codec_pflags & PFLG_FLUSH ) { WAKEUP( playback ); }
  codec_pflags &= ~( PFLG_USED | PFLG_FLUSH );	/* done */
  CLI();
  codec_image.ic &= ~CODEC_PLAYBACK_ENABLE;
  codec_out( CODEC_MCE | CODEC_IFACE_CTRL, codec_image.ic );
  OUTB( 0, CODECP( REGSEL ) );			/* shutdown MCE bit */
  STI();
}

static void codec_start_record( void )
{
  int size;
  
  codec_record_blk = 0;
  size = pcm_record.size << 1;
#if 0
  PRINTK( "codec_start_playback: start (size=0x%x)\n", size );
#endif
  CLI();
  disable_dma( gus_dma1 );
  if ( max_flag && gus_dma1 > 3 )
    {
      OUTB( max_cntrl_val & ~0x10, GUSP( MAXCNTRLPORT ) );
      OUTB( max_cntrl_val, GUSP( MAXCNTRLPORT ) );
    }
  clear_dma_ff( gus_dma1 );
  set_dma_mode( gus_dma1, DMA_MODE_READ | 0x10 );
  enable_dma( gus_dma1 );
  set_dma_addr( gus_dma1, (int)pcm_record.buf[ 0 ] );
  set_dma_count( gus_dma1, size );
  if ( pcm_mode & PCM_MODE_ADPCM ) 
    size >>= 2; 
   else
    {
      if ( pcm_mode & PCM_MODE_16 ) size >>= 1;
      if ( pcm_mode & PCM_MODE_STEREO ) size >>= 1;
    }
  size--;
#if 0
  PRINTK( "codec_start_playback: cnt (size=0x%x)\n", size );
#endif
  codec_out( CODEC_REC_LWR_CNT, (unsigned char)size );
  codec_out( CODEC_REC_UPR_CNT, (unsigned char)(size >> 8) );
  codec_image.ic |= CODEC_RECORD_ENABLE;
  codec_out( CODEC_IFACE_CTRL, codec_image.ic );	/* go! */
  codec_record_timer = -1;
  codec_record_timer_new = codec_get_timer_value( 1 );
  codec_timer_schedule();
  STI();
  codec_rflags |= RFLG_USED;	/* timer IRQs are accepted from this point */
}

static void codec_stop_record( void )
{
  codec_record_timer = codec_record_timer_new = -1;
  codec_timer_schedule();
  if ( codec_rflags & PFLG_FLUSH ) { WAKEUP( record ); }
  codec_rflags &= ~( RFLG_USED | RFLG_FLUSH );	/* done */
  CLI();
  codec_image.ic &= ~CODEC_RECORD_ENABLE;
  codec_out( CODEC_MCE | CODEC_IFACE_CTRL, codec_image.ic );
  OUTB( 0, CODECP( REGSEL ) );			/* shutdown MCE bit */
  STI();
}

void codec_interrupt( unsigned char status )
{
  static in_interrupt = 0;
  short playback_timer_done, record_timer_done;
  short timer_schedule;

  if ( in_interrupt )
    PRINTK( "gus: Eiaaa, interrupt routine reentered - %d times (codec)!!!\n", in_interrupt );
  in_interrupt++;

#if 0
  PRINTK( "codec_interrupt: status=0x%x\n", status );
#endif

  playback_timer_done = record_timer_done = timer_schedule = 0;
  
  if ( ( status & CODEC_TIMER_IRQ ) && 
       ( ( codec_pflags & PFLG_USED ) || ( codec_rflags & RFLG_USED ) ) )
    {
      if ( codec_playback_timer > 0 )
        {
          codec_playback_timer -= codec_timer_value;
          if ( codec_playback_timer <= 0 ) playback_timer_done++;
        }
      if ( codec_record_timer > 0 )
        {
          codec_record_timer -= codec_timer_value;
          if ( codec_record_timer <= 0 ) record_timer_done++;
        }
      timer_schedule++;
    }
  
  if ( ( codec_pflags & PFLG_USED ) && 
       ( ( status & CODEC_PLAYBACK_IRQ ) || playback_timer_done ) )
    {
#if 0
      PRINTK( "residue: 0x%x\n", get_dma_residue( gus_dma2 ) );
#endif
      pcm_playback.used--;
      pcm_playback.tsize[ codec_play_blk ] = 0;
      codec_play_blk ^= 1;
      if ( !pcm_playback.used )
        codec_stop_playback();
       else
        {
          if ( !codec_play_blk )
            {
              codec_playback_timer_new = codec_get_timer_value( 0 );
              timer_schedule++;
            }
        }
      if ( pcm_playback.flags & PCM_FLG_SLEEP )
        {
          pcm_playback.flags &= ~PCM_FLG_SLEEP;
          WAKEUP( playback );
        }
    }
  if ( ( codec_rflags & RFLG_USED ) &&
       ( ( status & CODEC_RECORD_IRQ ) || record_timer_done ) )
    {
      /* step 1 - check for end of block */
#if 0
      PRINTK( "residue: 0x%x\n", get_dma_residue( gus_dma1 ) );
#endif
      if ( !(codec_rflags & RFLG_FLUSH) )
        {
          pcm_record.used++;
          if ( pcm_record.used > 2 )
            PRINTK( "pcm: warning! record overflow\n" );
          pcm_record.tsize[ codec_record_blk ] = pcm_record.size;
        }
      codec_record_blk ^= 1;
      if ( pcm_record.flags & PCM_FLG_SLEEP )
        {
          pcm_record.flags &= ~PCM_FLG_SLEEP;
          WAKEUP( record );
        }
      if ( !codec_record_blk )
        {
          codec_record_timer_new = codec_get_timer_value( 1 );
          timer_schedule++;
        }
      if ( codec_rflags & RFLG_FLUSH )	/* stop this as soon is possible */
        codec_stop_record();
    }

  if ( timer_schedule )
    codec_timer_schedule();

  CLI();
  codec_out( CODEC_IRQ_STATUS, 
    ( CODEC_PLAYBACK_IRQ | CODEC_RECORD_IRQ | CODEC_TIMER_IRQ ) & ~status );
  OUTB( 0, CODECP( STATUS ) );
  STI();
  in_interrupt--;
#if 0
  PRINTK( "codec_interrupt: end\n" );
#endif
}

/*
 *
 */
 
void codec_init_to_dma( void )
{
  if ( !pcm_playback.used ) return;
  if ( !( codec_pflags & PFLG_USED ) )
    {
      codec_set_freq();
      codec_set_play_format();
      codec_start_playback();
    }
}

void codec_done_to_dma( void )
{
  if ( codec_pflags & PFLG_USED )
    {
      codec_pflags |= PFLG_FLUSH;
      while ( codec_pflags & PFLG_USED )
        {
          SLEEP( playback, HZ * 10 );
          if ( codec_pflags & PFLG_USED )
            {
              if ( TABORT( playback ) )
                pcm_playback.flags |= PCM_FLG_ABORT;
               else
              if ( TIMEOUT( playback ) )
                PRINTK( "pcm: flush failed (playback)\n" );
               else
                continue;
              codec_pflags = PFLG_NONE;
            }
        }
      codec_pflags = PFLG_NONE;
    }
}

void codec_init_from_dma( void )
{
  if ( !( codec_rflags & RFLG_USED ) )
    {
      codec_set_freq();
      codec_set_rec_format();
      codec_start_record();
    }
}

void codec_done_from_dma( void )
{
  if ( codec_rflags & RFLG_USED )
    {
#if 0          
      codec_stop_record();
#else
      codec_rflags |= RFLG_FLUSH;
      while ( codec_rflags & RFLG_USED )
        {
          SLEEP( record, HZ * 10 );
          if ( codec_rflags & RFLG_USED )
            {
              if ( TABORT( record ) )
                pcm_record.flags |= PCM_FLG_ABORT;
               else
              if ( TIMEOUT( record ) )
                PRINTK( "pcm: flush failed (record)\n" );
               else
                continue;
              codec_rflags = PFLG_NONE;
            }
        }
#endif
      codec_rflags = RFLG_NONE;
    }
}
