/*
 *  Copyright (c) by Jaroslav Kysela (Perex soft)
 *  Routines for control of CS4231(A)/InterWave chip
 *
 *  Note: Code isn't tested with CS4231 version of chip, only with CS4231A.
 *
 *
 *   This program is free software; you can redistribute it and/or modify
 *   it under the terms of the GNU General Public License as published by
 *   the Free Software Foundation; either version 2 of the License, or
 *   (at your option) any later version.
 *
 *   This program is distributed in the hope that it will be useful,
 *   but WITHOUT ANY WARRANTY; without even the implied warranty of
 *   MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
 *   GNU General Public License for more details.
 *
 *   You should have received a copy of the GNU General Public License
 *   along with this program; if not, write to the Free Software
 *   Foundation, Inc., 675 Mass Ave, Cambridge, MA 02139, USA.
 *
 */
  
#define __SND_OSS_COMPAT__
#define SND_MAIN_OBJECT_FILE
#include "driver.h"
#include "pcm.h"
#include "mixer.h"
#include "cs4231.h"
#include "ulaw.h"

#if 0
#define SND_DEBUG_MCE
#endif

/*
 *  Some variables
 */

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

static struct snd_stru_cs4231_image snd_cs4231_original_image = {
  0x00,						/* 00 - lic */
  0x00,						/* 01 - ric */
  0x80,						/* 02 - la1ic */
  0x80,						/* 03 - ra1ic */
  0x80,						/* 04 - la2ic */
  0x80,						/* 05 - ra2ic */
  0x80,						/* 06 - loc */
  0x80,						/* 07 - roc */
  0x20,						/* 08 - pdfr */
  CS4231_AUTOCALIB,				/* 09 - ic */
  0x00,						/* 0a - pc */
  0x00,						/* 0b - ti */
  CS4231_MODE2,					/* 0c - mi */
  0x00,						/* 0d - lbc */
  0x00,						/* 0e - pbru */
  0x00,						/* 0f - pbrl */
  0x80,						/* 10 - afei */
  0x01,						/* 11 - afeii */
  0x00,						/* 12 - llic */
  0x00,						/* 13 - rlic */
  0x00,						/* 14 - tlb */
  0x00,						/* 15 - thb */
  0x80,						/* 16 - la3ic */
  0x80,						/* 17 - ra3ic */
  0x00,						/* 18 - afs */
  0x80,						/* 19 - lamic */
  0xc0,						/* 1a - mioc */
  0x80,						/* 1b - ramic */
  0x20,						/* 1c - cdfr */
  0x00,						/* 1d - res1 */ 
  0x00,						/* 1e - cbru */
  0x00,						/* 1f - cbrl */
};

/*
 *  Basic I/O functions
 */
 
static void snd_cs4231_outm( cs4231_t *codec, unsigned char reg, unsigned char mask, unsigned char value )
{
  int timeout;
  unsigned char tmp;

  for ( timeout = 250; timeout > 0 && (inb( CS4231P( codec, REGSEL ) ) & CS4231_INIT); timeout-- )
    snd_delay( 1 );
#ifdef SNDCFG_DEBUG
  if ( inb( CS4231P( codec, REGSEL ) ) & CS4231_INIT )
    snd_printk( "snd_cs4231_outm: auto calibration time out - reg = 0x%x, value = 0x%x\n", reg, value );
#endif
  outb( codec -> mce_bit | reg, CS4231P( codec, REGSEL ) ); mb();
  tmp = ( inb( CS4231P( codec, REG ) ) & mask ) | value;
  outb( tmp, CS4231P( codec, REG ) ); mb();
}

static void snd_cs4231_out( cs4231_t *codec, unsigned char reg, unsigned char value )
{
  int timeout;

  for ( timeout = 250; timeout > 0 && (inb( CS4231P( codec, REGSEL ) ) & CS4231_INIT); timeout-- )
    snd_delay( 1 );
#ifdef SNDCFG_DEBUG
  if ( inb( CS4231P( codec, REGSEL ) ) & CS4231_INIT )
    snd_printk( "snd_cs4231_out: auto calibration time out - reg = 0x%x, value = 0x%x\n", reg, value );
#endif
  outb( codec -> mce_bit | reg, CS4231P( codec, REGSEL ) );
  outb( value, CS4231P( codec, REG ) ); mb();
#if 0
  printk( "codec out - reg 0x%x = 0x%x\n", codec -> mce_bit | reg, value );
#endif
}
    
static unsigned char snd_cs4231_in( cs4231_t *codec, unsigned char reg )
{
  int timeout;

  for ( timeout = 250; timeout > 0 && (inb( CS4231P( codec, REGSEL ) ) & CS4231_INIT); timeout-- )
    snd_delay( 1 );
#ifdef SNDCFG_DEBUG
  if ( inb( CS4231P( codec, REGSEL ) ) & CS4231_INIT )
    snd_printk( "snd_cs4231_in: auto calibration time out - reg = 0x%x\n", reg );
#endif
  outb( codec -> mce_bit | reg, CS4231P( codec, REGSEL ) ); mb();
  return inb( CS4231P( codec, REG ) );
}

#ifdef SNDCFG_DEBUG

void snd_cs4231_debug( cs4231_t *codec )
{
  printk( "CS4231 REGS:      INDEX = 0x%02x  ", inb( CS4231P( codec, REGSEL ) ) );
  printk( "                 STATUS = 0x%02x\n", inb( CS4231P( codec, STATUS ) ) );
  printk( "  0x00: left input      = 0x%02x  ", snd_cs4231_in( codec, 0x00 ) );
  printk( "  0x10: alt 1 (CFIG 2)  = 0x%02x\n", snd_cs4231_in( codec, 0x10 ) );
  printk( "  0x01: right input     = 0x%02x  ", snd_cs4231_in( codec, 0x01 ) );
  printk( "  0x11: alt 2 (CFIG 3)  = 0x%02x\n", snd_cs4231_in( codec, 0x11 ) );
  printk( "  0x02: GF1 left input  = 0x%02x  ", snd_cs4231_in( codec, 0x02 ) );
  printk( "  0x12: left line in    = 0x%02x\n", snd_cs4231_in( codec, 0x12 ) );
  printk( "  0x03: GF1 right input = 0x%02x  ", snd_cs4231_in( codec, 0x03 ) );
  printk( "  0x13: right line in   = 0x%02x\n", snd_cs4231_in( codec, 0x13 ) );
  printk( "  0x04: CD left input   = 0x%02x  ", snd_cs4231_in( codec, 0x04 ) );
  printk( "  0x14: timer low       = 0x%02x\n", snd_cs4231_in( codec, 0x14 ) );
  printk( "  0x05: CD right input  = 0x%02x  ", snd_cs4231_in( codec, 0x05 ) );
  printk( "  0x15: timer high      = 0x%02x\n", snd_cs4231_in( codec, 0x15 ) );
  printk( "  0x06: left output     = 0x%02x  ", snd_cs4231_in( codec, 0x06 ) );
  printk( "  0x16: left MIC (PnP)  = 0x%02x\n", snd_cs4231_in( codec, 0x16 ) );
  printk( "  0x07: right output    = 0x%02x  ", snd_cs4231_in( codec, 0x07 ) );
  printk( "  0x17: right MIC (PnP) = 0x%02x\n", snd_cs4231_in( codec, 0x17 ) );
  printk( "  0x08: playback format = 0x%02x  ", snd_cs4231_in( codec, 0x08 ) );
  printk( "  0x18: IRQ status      = 0x%02x\n", snd_cs4231_in( codec, 0x18 ) );
  printk( "  0x09: iface (CFIG 1)  = 0x%02x  ", snd_cs4231_in( codec, 0x09 ) );
  printk( "  0x19: left line out   = 0x%02x\n", snd_cs4231_in( codec, 0x19 ) );
  printk( "  0x0a: pin control     = 0x%02x  ", snd_cs4231_in( codec, 0x0a ) );
  printk( "  0x1a: mono control    = 0x%02x\n", snd_cs4231_in( codec, 0x1a ) );
  printk( "  0x0b: init & status   = 0x%02x  ", snd_cs4231_in( codec, 0x0b ) );
  printk( "  0x1b: right line out  = 0x%02x\n", snd_cs4231_in( codec, 0x1b ) );
  printk( "  0x0c: revision & mode = 0x%02x  ", snd_cs4231_in( codec, 0x0c ) );
  printk( "  0x1c: record format   = 0x%02x\n", snd_cs4231_in( codec, 0x1c ) );
  printk( "  0x0d: loopback        = 0x%02x  ", snd_cs4231_in( codec, 0x0d ) );
  printk( "  0x1d: var freq (PnP)  = 0x%02x\n", snd_cs4231_in( codec, 0x1d ) );
  printk( "  0x0e: ply upr count   = 0x%02x  ", snd_cs4231_in( codec, 0x0e ) );
  printk( "  0x1e: ply lwr count   = 0x%02x\n", snd_cs4231_in( codec, 0x1e ) );
  printk( "  0x0f: rec upr count   = 0x%02x  ", snd_cs4231_in( codec, 0x0f ) );
  printk( "  0x1f: rec lwr count   = 0x%02x\n", snd_cs4231_in( codec, 0x1f ) );
}

#endif

/*
 *  CS4231 detection / MCE routines
 */
  
static void snd_cs4231_mce_up( cs4231_t *codec )
{
  unsigned long flags;
  int timeout;
  
  for ( timeout = 250; timeout > 0 && (inb( CS4231P( codec, REGSEL ) ) & CS4231_INIT); timeout-- )
    snd_delay( 1 );
#ifdef SNDCFG_DEBUG
  if ( inb( CS4231P( codec, REGSEL ) ) & CS4231_INIT )
    snd_printk( "cs4231_mce_up - auto calibration time out (0)\n" );
#endif
  snd_spin_lock( codec, mce, &flags );
  codec -> mce_bit |= CS4231_MCE;
  timeout = inb( CS4231P( codec, REGSEL ) );
  if ( timeout == 0x80 )
    snd_printk( "snd_cs4231_up [0x%x]: serious init problem - codec still busy\n", codec -> port );
  if ( !(timeout & CS4231_MCE) )
    outb( codec -> mce_bit | ( timeout & 0x1f ), CS4231P( codec, REGSEL ) );
  snd_spin_unlock( codec, mce, &flags );
}
  
static void snd_cs4231_mce_down( cs4231_t *codec )
{
  unsigned long flags;
  int timeout;
  unsigned long time;

  snd_spin_lock( codec, mce, &flags );
  /* huh.. looks like this sequence is proper for CS4231A chip (GUS MAX) */
  for ( timeout = 5; timeout > 0; timeout-- )
    inb( CS4231P( codec, REGSEL ) );
  /* end of cleanup sequence */
  for ( timeout = 250; timeout > 0 && (inb( CS4231P( codec, REGSEL ) ) & CS4231_INIT); timeout-- )
    snd_delay( 1 );
#if 0
  printk( "(1) timeout = %i\n", timeout );
#endif
#ifdef SNDCFG_DEBUG
  if ( inb( CS4231P( codec, REGSEL ) ) & CS4231_INIT )
    snd_printk( "cs4231_mce_down [0x%x] - auto calibration time out (0)\n", CS4231P( codec, REGSEL ) );
#endif
  codec -> mce_bit &= ~CS4231_MCE;
  timeout = inb( CS4231P( codec, REGSEL ) );
  outb( codec -> mce_bit | ( timeout & 0x1f ), CS4231P( codec, REGSEL ) );
  if ( timeout == 0x80 )
    snd_printk( "snd_cs4231_down [0x%x]: serious init problem - codec still busy\n", codec -> port );
  if ( (timeout & CS4231_MCE) == 0 || codec -> hardware == CS4231_HW_INTERWAVE ) {
    snd_spin_unlock( codec, mce, &flags );
    return;
  }

  /* calibration process */

  for ( timeout = 500; timeout > 0 && (snd_cs4231_in( codec, CS4231_TEST_INIT ) & CS4231_CALIB_IN_PROGRESS) == 0; timeout-- );
  if ( ( snd_cs4231_in( codec, CS4231_TEST_INIT ) & CS4231_CALIB_IN_PROGRESS ) == 0 ) {
    snd_printd( "cs4231_mce_down - auto calibration time out (1)\n" );
    snd_spin_unlock( codec, mce, &flags );
    return;
  }
#if 0
  printk( "(2) timeout = %i, jiffies = %li\n", timeout, jiffies );
#endif
  time = jiffies + (HZ/4);
  while ( snd_cs4231_in( codec, CS4231_TEST_INIT ) & CS4231_CALIB_IN_PROGRESS ) {
    snd_spin_unlock( codec, mce, &flags );
    if ( (signed long)(jiffies - time) >= 0 ) {
      snd_printk( "cs4231_mce_down - auto calibration time out (2)\n" );
      return;
    }
    schedule();
    snd_spin_lock( codec, mce, &flags );    
  }
#if 0
  printk( "(3) jiffies = %li\n", jiffies );
#endif
  time = jiffies + (HZ/10);
  while ( inb( CS4231P( codec, REGSEL ) ) & CS4231_INIT ) {
    snd_spin_unlock( codec, mce, &flags );
    if ( (signed long)(jiffies - time) >= 0 ) {
      snd_printk( "cs4231_mce_down - auto calibration time out (3)\n" );
      return;
    }
    schedule();
    snd_spin_lock( codec, mce, &flags );    
  }
  snd_spin_unlock( codec, mce, &flags );
#if 0
  printk( "(4) jiffies = %li\n", jiffies );
  snd_printk( "cs4231_mce_down - exit = 0x%x\n", inb( CS4231P( codec, REGSEL ) ) );
#endif
}

static unsigned int snd_cs4231_get_count( unsigned char format, unsigned int size )
{
  switch ( format & 0xe0 ) {
    case CS4231_LINEAR_16:
    case CS4231_LINEAR_16_BIG:
      size >>= 1;
      break;
    case CS4231_ADPCM_16:
      return size >> 2;
  }
  if ( format & CS4231_STEREO ) size >>= 1;
  return size;
}

static void snd_cs4231_trigger( cs4231_t *codec, unsigned char what, int enable )
{
  unsigned long flags;

#if 0
  printk( "codec trigger!!! - what = %i, enable = %i, status = 0x%x\n", what, enable, inb( CS4231P( card, STATUS ) ) );
#endif
  snd_spin_lock( codec, reg, &flags );
  if ( enable )
    {
      if ( codec -> image.ic & what ) {
        snd_spin_unlock( codec, reg, &flags );
        return;
      }
      snd_cs4231_out( codec, CS4231_IFACE_CTRL, codec -> image.ic |= what );
#if 0
      snd_cs4231_out( codec, CS4231_ALT_FEATURE_1, codec -> image.afei &= ~CS4231_DACZ );
#endif
    }
   else
    {
      if ( !(codec -> image.ic & what) ) {
        snd_spin_unlock( codec, reg, &flags );
        return;
      }
      snd_cs4231_out( codec, CS4231_IFACE_CTRL, codec -> image.ic &= ~what );
#if 0
      snd_cs4231_out( codec, CS4231_ALT_FEATURE_1, codec -> image.afei |= CS4231_DACZ );
#endif
    }
#if 0
  /* fix for CS 4231 */
  if ( codec -> hardware != CS4231_HW_INTERWAVE ) {
#if 0
    snd_cs4231_mce_up( codec );
    snd_cs4231_mce_down( codec );
#endif
    if ( (codec -> image.ic & (CS4231_PLAYBACK_ENABLE|CS4231_RECORD_ENABLE)) ||
         (codec -> image.afei & CS4231_TIMER_ENABLE) )
      codec -> image.pc |= CS4231_IRQ_ENABLE;
     else
      codec -> image.pc &= ~CS4231_IRQ_ENABLE;
    snd_cs4231_out( codec, CS4231_PIN_CTRL, codec -> image.pc );
  }
#endif
  snd_spin_unlock( codec, reg, &flags );
#if 0
  snd_cs4231_debug( codec );
#endif
}
 
/*
 *  CODEC I/O
 */

static unsigned char snd_cs4231_get_freq( unsigned int freq ) /* freq in Hz */
{
  int i;
  
  freq /= 1000;
#if 0
  snd_printk( "pcm_rate: %d\n", freq );
#endif
  if ( freq > 48 ) freq = 48; 
  for ( i = 0; i < 14; i++ )
    if ( freq <= snd_cs4231_freq[ i ].hertz )
      return snd_cs4231_freq[ i ].bits;
  return snd_cs4231_freq[ 13 ].bits;
}

static unsigned int snd_cs4231_get_real_freq( unsigned int freq ) /* freq in Hz */
{
  int i;
  
  freq /= 1000;
#if 0
  snd_printk( "pcm_rate: %d\n", freq );
#endif
  if ( freq > 48 ) freq = 48; 
  for ( i = 0; i < 14; i++ )
    if ( freq <= snd_cs4231_freq[ i ].hertz )
      return snd_cs4231_freq[ i ].rate;
  return snd_cs4231_freq[ 13 ].rate;
}

static void snd_cs4231_playback_compute_rate( snd_pcm_t *pcm )
{
  pcm -> playback.real_rate = snd_cs4231_get_real_freq( pcm -> playback.rate );
}

static void snd_cs4231_record_compute_rate( snd_pcm_t *pcm )
{
  pcm -> record.real_rate = snd_cs4231_get_real_freq( pcm -> record.rate );
}

static unsigned char snd_cs4231_get_format( unsigned int mode, int voices )
{
  unsigned char format;

  format = 0;
  if ( mode & SND_PCM_MODE_16 )
    {
      if ( mode & SND_PCM_MODE_ADPCM )
        format |= CS4231_ADPCM_16;
       else
      if ( mode & SND_PCM_MODE_BIG )
        format |= CS4231_LINEAR_16_BIG;
       else
        format |= CS4231_LINEAR_16;
    }
   else
    {
      if ( mode & SND_PCM_MODE_ALAW )
        format |= CS4231_ALAW_8;
       else
      if ( mode & SND_PCM_MODE_ULAW )
        format |= CS4231_ULAW_8;
#if 0
       else
        format |= CS4231_LINEAR_8;	/* I know, OR with 0 */
#endif
    }
  if ( voices > 1 )
    format |= CS4231_STEREO;
#if 0
  snd_printk( "snd_cs4231_get_format: 0x%x (mode=0x%x)\n", format, mode );
#endif
  return format;
}

static void snd_cs4231_set_play_format( cs4231_t *codec )
{
  unsigned int mode;
  snd_pcm_channel_t *pchn;

  pchn = &codec -> pcm -> playback;
  mode = pchn -> mode;
  if ( (mode & SND_PCM_MODE_ULAW) &&
       codec -> hardware == CS4231_HW_INTERWAVE &&
       codec -> dma1 > 3 ) {
    mode = SND_PCM_MODE_U;
  }
  codec -> image.pdfr = 
    snd_cs4231_get_format( mode, pchn -> voices ) |
    snd_cs4231_get_freq( pchn -> real_rate );
#if 0
  snd_printk( ">>> pmode = 0x%x, pdfr = 0x%x\n", mode, codec -> image.pdfr );
#endif
}
 
static void snd_cs4231_set_rec_format( cs4231_t *codec )
{
  snd_pcm_channel_t *pchn;

  pchn = &codec -> pcm -> record;
  codec -> image.cdfr = 
    snd_cs4231_get_format( pchn -> mode, pchn -> voices ) |
    snd_cs4231_get_freq( pchn -> real_rate );
#if 0
  snd_printk( ">>> rmode = 0x%x, cdfr = 0x%x\n", card -> pcm.chn[ PCM_RECORD ].mode, card -> codec.image.cdfr );
#endif
}

#if 0

/*
 *  prepared for future use
 */

static void snd_cs4231_timer_start( cs4231_t *codec, unsigned int value )
{
  unsigned long flags;

  if ( value > 0xffff ) value = 0xffff;
  snd_spin_lock( codec, reg, &flags );
  codec -> image.afei |= CS4231_TIMER_ENABLE;
  snd_cs4231_out( card, CS4231_TIMER_LOW, (unsigned char)value );
  snd_cs4231_out( card, CS4231_TIMER_HIGH, (unsigned char)( value >> 8 ) );
  snd_cs4231_out( card, CS4231_ALT_FEATURE_1, codec -> image.afei );
  snd_spin_unlock( codec, reg, &flags );
  codec -> timer_value = value;
}

static void cs4231_timer_stop( snd_card_t *card )
{
  unsigned long flags;

  snd_spin_lock( codec, reg, &flags );
  codec -> image.afei &= ~CS4231_TIMER_ENABLE;
  snd_cs4231_out( card, CS4231_ALT_FEATURE_1, codec -> image.afei );
  snd_spin_unlock( codec, reg, &flags );
}

#endif

static void snd_cs4231_open( cs4231_t *codec )
{
  unsigned long flags;

  if ( codec -> mode & CS4231_MODE_OPEN ) return;

  snd_cs4231_mce_down( codec );

#ifdef SND_DEBUG_MCE
  snd_printk( "snd_cs4231_open: (1)\n" );
#endif
  snd_cs4231_mce_up( codec );
  snd_spin_lock( codec, reg, &flags );
  codec -> image.ic &= ~(CS4231_PLAYBACK_ENABLE|CS4231_PLAYBACK_PIO|
                         CS4231_RECORD_ENABLE|CS4231_RECORD_PIO|
                         CS4231_CALIB_MODE);
  codec -> image.ic |= CS4231_AUTOCALIB;
  snd_cs4231_out( codec, CS4231_IFACE_CTRL, codec -> image.ic );
  snd_spin_unlock( codec, reg, &flags );
  snd_cs4231_mce_down( codec );

#ifdef SND_DEBUG_MCE
  snd_printk( "snd_cs4231_open: (2)\n" );
#endif

  snd_cs4231_mce_up( codec );  
  snd_spin_lock( codec, reg, &flags );
  snd_cs4231_out( codec, CS4231_ALT_FEATURE_1, codec -> image.afei );
  snd_spin_unlock( codec, reg, &flags );
  snd_cs4231_mce_down( codec ); 

#ifdef SND_DEBUG_MCE
  snd_printk( "snd_cs4231_open: (3) - afei = 0x%x\n", codec -> image.afei );
#endif

  snd_spin_lock( codec, reg, &flags );
  snd_cs4231_out( codec, CS4231_ALT_FEATURE_2, codec -> image.afeii );
  snd_spin_unlock( codec, reg, &flags );

  snd_cs4231_set_rec_format( codec );
  snd_cs4231_set_play_format( codec );

  snd_cs4231_mce_up( codec );
  snd_spin_lock( codec, reg, &flags );
  snd_cs4231_out( codec, CS4231_PLAYBK_FORMAT, codec -> image.pdfr );
  snd_spin_unlock( codec, reg, &flags );
  snd_cs4231_mce_down( codec );

#ifdef SND_DEBUG_MCE
  snd_printk( "snd_cs4231_open: (4)\n" );
#endif

  snd_cs4231_mce_up( codec );
  snd_spin_lock( codec, reg, &flags );
  snd_cs4231_out( codec, CS4231_REC_FORMAT,
			codec -> hardware == CS4231_HW_INTERWAVE ?
				codec -> image.cdfr :
				(codec -> image.pdfr & 0xf0) );
  snd_spin_unlock( codec, reg, &flags );
  snd_cs4231_mce_down( codec );

#ifdef SND_DEBUG_MCE
  snd_printk( "snd_cs4231_open: (5)\n" );
#endif

  /* ok. now enable and ack CODEC IRQ */
  snd_spin_lock( codec, reg, &flags );
  snd_cs4231_out( codec, CS4231_IRQ_STATUS, CS4231_PLAYBACK_IRQ | 
                            	              CS4231_RECORD_IRQ | 
                                              CS4231_TIMER_IRQ );
  snd_cs4231_out( codec, CS4231_IRQ_STATUS, 0 );
  outb( 0, CS4231P( codec, STATUS ) );		/* clear IRQ */
  outb( 0, CS4231P( codec, STATUS ) );		/* clear IRQ */
  codec -> image.pc |= CS4231_IRQ_ENABLE;
  snd_cs4231_out( codec, CS4231_PIN_CTRL, codec -> image.pc );
  snd_cs4231_out( codec, CS4231_IRQ_STATUS, CS4231_PLAYBACK_IRQ | 
                                              CS4231_RECORD_IRQ | 
                                              CS4231_TIMER_IRQ );
  snd_cs4231_out( codec, CS4231_IRQ_STATUS, 0 );
  snd_spin_unlock( codec, reg, &flags );
}

static void snd_cs4231_close( cs4231_t *codec )
{
  unsigned long flags;

  if ( codec -> mode & CS4231_MODE_OPEN ) return;

  /* disable IRQ */
  snd_spin_lock( codec, reg, &flags );
  snd_cs4231_out( codec, CS4231_IRQ_STATUS, 0 );
  outb( 0, CS4231P( codec, STATUS ) );		/* clear IRQ */
  outb( 0, CS4231P( codec, STATUS ) );		/* clear IRQ */
  codec -> image.pc &= ~CS4231_IRQ_ENABLE;
  snd_cs4231_out( codec, CS4231_PIN_CTRL, codec -> image.pc );
  snd_spin_unlock( codec, reg, &flags );

  /* now disable record & playback */

  snd_cs4231_mce_up( codec );
  snd_spin_lock( codec, reg, &flags );
  codec -> image.ic &= ~(CS4231_PLAYBACK_ENABLE|CS4231_PLAYBACK_PIO|
                         CS4231_RECORD_ENABLE|CS4231_RECORD_PIO);
  snd_cs4231_out( codec, CS4231_IFACE_CTRL, codec -> image.ic );
  snd_spin_unlock( codec, reg, &flags );
  snd_cs4231_mce_down( codec );

  /* clear IRQ again */
  snd_spin_lock( codec, reg, &flags );
  snd_cs4231_out( codec, CS4231_IRQ_STATUS, 0 );
  outb( 0, CS4231P( codec, STATUS ) );		/* clear IRQ */
  outb( 0, CS4231P( codec, STATUS ) );		/* clear IRQ */
  snd_spin_unlock( codec, reg, &flags );
}

/*
 *  ok.. exported functions..
 */

static void snd_cs4231_playback_trigger( snd_pcm_t *pcm, int up )
{
  cs4231_t *codec;
  
  codec = (cs4231_t *)pcm -> private_data;

  if ( codec -> mixer && !up )
    snd_mixer_set_kernel_mute( codec -> mixer, SND_MIXER_PRI_PCM, SND_MIX_MUTE );

  snd_cs4231_trigger( codec, CS4231_PLAYBACK_ENABLE, up );

  if ( codec -> mixer && up )
    snd_mixer_set_kernel_mute( codec -> mixer, SND_MIXER_PRI_PCM, 0 );

#if 0
  if ( up )
    snd_cs4231_debug( codec );
#endif
}

static void snd_cs4231_record_trigger( snd_pcm_t *pcm, int up )
{
  cs4231_t *codec;
  
  codec = (cs4231_t *)pcm -> private_data;
  snd_cs4231_trigger( codec, CS4231_RECORD_ENABLE, up );
}

static void snd_cs4231_playback_prepare( snd_pcm_t *pcm,
                                         unsigned char *buffer,
                                         unsigned int size,
                                         unsigned int offset,
                                         unsigned int count )
{
  cs4231_t *codec;
  unsigned long flags;
  
  codec = (cs4231_t *)pcm -> private_data;
  /* Note: offset is always 0 if AUTO DMA */
  snd_cs4231_set_play_format( codec );
#if 0	/* TODO!!! Move this code to interwave.c... */
  switch ( codec -> interwave_serial_port ) {
    case ( 2 << 5 ) | 0x1f:
    case ( 3 << 5 ) | 0x1f:
      snd_i_write8( card, 0x59, codec -> interwave_serial_port );
      break;
  }
#endif
#if 0
  snd_printk( "cs4231_start_playback - dma = %i, buffer = 0x%lx, size = 0x%x, offset = 0x%x, count = 0x%x\n", codec -> dma1, (long)buffer, size, offset, count );
#endif
  codec -> image.ic &= ~(CS4231_PLAYBACK_ENABLE|CS4231_PLAYBACK_PIO);
  snd_dma_program( codec -> dma1, buffer, size, DMA_MODE_WRITE | DMA_MODE_AUTOINIT );
  count = snd_cs4231_get_count( codec -> image.pdfr, count ) - 1;
  snd_cs4231_mce_up( codec );
  snd_spin_lock( codec, reg, &flags );
  if ( codec -> hardware != CS4231_HW_INTERWAVE && !codec -> single_dma ) {
    snd_cs4231_out( codec, CS4231_PLAYBK_FORMAT,
    	(codec -> image.ic & CS4231_RECORD_ENABLE) ?
    		(codec -> image.pdfr & 0xf0) | (codec -> image.cdfr & 0x0f) :
    		codec -> image.pdfr );
    if ( !codec -> single_dma )
      snd_cs4231_out( codec, CS4231_REC_FORMAT, codec -> image.cdfr & 0xf0 );
  } else {
    snd_cs4231_out( codec, CS4231_PLAYBK_FORMAT, codec -> image.pdfr );
  }
#if 0
  printk( "ic = 0x%x, REC_FORMAT = 0x%x (0x%x), PLAYBK_FORMAT = 0x%x (0x%x)\n", codec -> image.ic, snd_cs4231_in( codec, CS4231_REC_FORMAT ), codec -> image.cdfr, snd_cs4231_in( codec, CS4231_PLAYBK_FORMAT ), codec -> image.pdfr );
#endif
  snd_spin_unlock( codec, reg, &flags );
  snd_cs4231_mce_down( codec );
  snd_spin_lock( codec, reg, &flags );
  snd_cs4231_out( codec, CS4231_PLY_LWR_CNT, (unsigned char)count );
  snd_cs4231_out( codec, CS4231_PLY_UPR_CNT, (unsigned char)(count >> 8) );
  snd_spin_unlock( codec, reg, &flags );
#if 0
  snd_cs4231_debug( codec );
#endif
}

static void snd_cs4231_record_prepare( snd_pcm_t *pcm,
					 unsigned char *buffer,
					 unsigned int size,
					 unsigned int offset,
					 unsigned int count )
{
  cs4231_t *codec;
  unsigned long flags;
  
  codec = (cs4231_t *)pcm -> private_data;
  snd_cs4231_set_rec_format( codec );
#if 0  /* TODO!!! Move to interwave.c... */
  switch ( card -> codec.interwave_serial_port ) {
    case ( 1 << 5 ) | 0x1f:
    case ( 5 << 5 ) | 0x1f:
      snd_i_write8( card, 0x59, card -> codec.interwave_serial_port );
      break;
  }
#endif
#if 0
  snd_printk( "cs4231_start_record: start - buffer = 0x%lx, offset = 0x%x, size = 0x%x, count = 0x%x\n", (long)buffer, offset, size, count );
#endif
  codec -> image.ic &= ~(CS4231_RECORD_ENABLE|CS4231_RECORD_PIO);
  snd_dma_program( codec -> dma2, buffer, size, DMA_MODE_READ | DMA_MODE_AUTOINIT );
  count = snd_cs4231_get_count( codec -> image.cdfr, count ) - 1;
  snd_cs4231_mce_up( codec );
  snd_spin_lock( codec, reg, &flags );
  if ( codec -> hardware != CS4231_HW_INTERWAVE ) {
    if ( !(codec -> image.ic & CS4231_PLAYBACK_ENABLE) ) {
      snd_cs4231_out( codec, CS4231_PLAYBK_FORMAT, ( (codec -> single_dma ? codec -> image.cdfr : codec -> image.pdfr) & 0xf0 ) | ( codec -> image.cdfr & 0x0f ) );
    }
    snd_cs4231_out( codec, CS4231_REC_FORMAT, codec -> image.cdfr & 0xf0 );
  } else {
    snd_cs4231_out( codec, CS4231_REC_FORMAT, codec -> image.cdfr );
  }
#if 0
  printk( "ic = 0x%x, REC_FORMAT = 0x%x (0x%x), PLAYBK_FORMAT = 0x%x (0x%x)\n", codec -> image.ic, snd_cs4231_in( codec, CS4231_REC_FORMAT ), codec -> image.cdfr, snd_cs4231_in( codec, CS4231_PLAYBK_FORMAT ), codec -> image.pdfr );
#endif
  snd_spin_unlock( codec, reg, &flags );
  snd_cs4231_mce_down( codec );
#if 0
  printk( "cdfr = 0x%x, rport = 0x%x, pport = 0x%x, status = 0x%x\n", codec -> image.cdfr, snd_cs4231_in( codec, CS4231_REC_FORMAT ), snd_cs4231_in( codec, CS4231_PLAYBK_FORMAT ), inb( CS4231P( codec, REGSEL ) ) );
#endif
  snd_spin_lock( codec, reg, &flags );
  if ( codec -> single_dma && codec -> hardware != CS4231_HW_INTERWAVE ) {
    snd_cs4231_out( codec, CS4231_PLY_LWR_CNT, (unsigned char)count );
    snd_cs4231_out( codec, CS4231_PLY_UPR_CNT, (unsigned char)(count >> 8) );
  } else {
    snd_cs4231_out( codec, CS4231_REC_LWR_CNT, (unsigned char)count );
    snd_cs4231_out( codec, CS4231_REC_UPR_CNT, (unsigned char)(count >> 8) );
  }
  snd_spin_unlock( codec, reg, &flags );
}

void snd_cs4231_interrupt( snd_pcm_t *pcm, unsigned char status )
{
  unsigned long flags;
  cs4231_t *codec;

#if 0
  snd_printk( "snd_cs4231_interrupt: status=0x%x\n", status );
#endif
  codec = pcm -> private_data;
  if ( !codec ) return;
  status = snd_cs4231_in( codec, CS4231_IRQ_STATUS );

  if ( codec -> single_dma && codec -> hardware != CS4231_HW_INTERWAVE ) {
    if ( status & CS4231_PLAYBACK_IRQ ) {
      if ( (codec -> mode & CS4231_MODE_PLAY) && pcm -> playback.ack )
        pcm -> playback.ack( pcm );
      if ( (codec -> mode & CS4231_MODE_RECORD) && pcm -> record.ack )
        pcm -> record.ack( pcm );
    }
  } else {
    if ( (status & CS4231_PLAYBACK_IRQ) && pcm -> playback.ack )
      pcm -> playback.ack( pcm );
    if ( (status & CS4231_RECORD_IRQ) && pcm -> record.ack )
      pcm -> record.ack( pcm );
  }
 
  snd_spin_lock( codec, reg, &flags );
  outb( 0, CS4231P( codec, STATUS ) );	/* clear global interrupt bit */
  snd_cs4231_out( codec, CS4231_IRQ_STATUS, CS4231_ALL_IRQS & ~status );
  snd_spin_unlock( codec, reg, &flags );
}

/*
 *
 */

static int snd_cs4231_playback_open( snd_pcm_t *pcm )
{
  snd_card_t *card;
  cs4231_t *codec;
  int err;

  card = pcm -> card;
  codec = (cs4231_t *)pcm -> private_data;
  if ( (err = snd_pcm_dma_alloc( pcm, SND_PCM_PLAYBACK, codec -> dmanum1, "CS4231 (playback)" )) < 0 )
    return err;
  snd_cs4231_open( codec );
  codec -> mode |= CS4231_MODE_PLAY;
  return 0;
}

static int snd_cs4231_record_open( snd_pcm_t *pcm )
{
  snd_card_t *card;
  cs4231_t *codec;
  int err;

  card = pcm -> card;
  codec = (cs4231_t *)pcm -> private_data;
  if ( (err = snd_pcm_dma_alloc( pcm, SND_PCM_RECORD, codec -> dmanum2, "CS4231 (record)" )) < 0 )
    return err;
  snd_cs4231_open( codec );
  codec -> mode |= CS4231_MODE_RECORD;
  return 0;
}

static void snd_cs4231_playback_close( snd_pcm_t *pcm )
{
  snd_card_t *card;
  cs4231_t *codec;

  card = pcm -> card;
  codec = (cs4231_t *)pcm -> private_data;
  codec -> mode &= ~CS4231_MODE_PLAY; 
  snd_cs4231_close( codec );
  snd_pcm_dma_free( pcm, SND_PCM_PLAYBACK, codec -> dmanum1 );
}

static void snd_cs4231_record_close( snd_pcm_t *pcm )
{
  snd_card_t *card;
  cs4231_t *codec;

  card = pcm -> card;
  codec = (cs4231_t *)pcm -> private_data;
  codec -> mode &= ~CS4231_MODE_RECORD;
  snd_cs4231_close( codec );
  snd_pcm_dma_free( pcm, SND_PCM_RECORD, codec -> dmanum2 );
}

static void snd_cs4231_playback_dma( snd_pcm_t *pcm,
                                     unsigned char *buffer, unsigned int offset,
                                     unsigned char *user, unsigned int count )
{
  cs4231_t *codec;
  
  codec = (cs4231_t *)pcm -> private_data;
  if ( (pcm -> playback.mode & SND_PCM_MODE_ULAW) &&
       codec -> hardware == CS4231_HW_INTERWAVE && codec -> dma1 > 3 ) {
    snd_translate_from_user( snd_ulaw_dsp_loud, &buffer[ offset ], user, count );
  } else {
    copy_from_user( &buffer[ offset ], user, count );
  }
}
                                                              
static unsigned int snd_cs4231_playback_pointer( snd_pcm_t *pcm, unsigned int used_size )
{
  cs4231_t *codec;

  codec = (cs4231_t *)pcm -> private_data;
  if ( !(codec -> image.ic & CS4231_PLAYBACK_ENABLE) ) return 0;
  return used_size - snd_dma_residue( codec -> dma1 );
}

static unsigned int snd_cs4231_record_pointer( snd_pcm_t *pcm, unsigned int used_size )
{
  cs4231_t *codec;

  codec = (cs4231_t *)pcm -> private_data;
  if ( !(codec -> image.ic & CS4231_RECORD_ENABLE) ) return 0;
  return used_size - snd_dma_residue( codec -> dma2 );
}

/*
 *
 */

#if 0 
void snd_cs4231_info( snd_card_t *card, snd_info_buffer_t *buffer )
{
  if ( !card -> use_codec ) return;
  snd_iprintf( buffer, "CS4231:\n" );
  snd_iprintf( buffer, "  mode               : %s\n", card -> codec.hardware != CS4231_HW_INTERWAVE ? "2" : "3" );
  snd_iprintf( buffer, "  record overflow    : %i\n", card -> codec.record_overflow );
  if ( card -> pnp_flag )
    {
      snd_iprintf( buffer, "  playback fifo      : %i\n", card -> codec.playback_fifo_size );
      snd_iprintf( buffer, "  record fifo        : %i\n", card -> codec.record_fifo_size );
    }
}
#endif

/*
 *
 */

static int snd_cs4231_probe( snd_pcm_t *pcm )
{
  unsigned long flags;
  cs4231_t *codec;
  int i, id, rev;
  unsigned char *ptr;

  codec = (cs4231_t *)pcm -> private_data;
  if ( !codec ) return -EINVAL;

#if 0
  snd_cs4231_debug( codec );
#endif
  id = 0;
  for ( i = 0; i < 1000; i++ )
    {
      mb();
      if ( inb( CS4231P( codec, REGSEL ) ) & CS4231_INIT )
        snd_delay( 50 * 8 );
       else
        {
          snd_spin_lock( codec, reg, &flags );
          snd_cs4231_out( codec, CS4231_MISC_INFO, CS4231_MODE2 );
	  id = snd_cs4231_in( codec, CS4231_MISC_INFO ) & 0x0f;
	  snd_spin_unlock( codec, reg, &flags );
          if ( id == 0x0a ) break;	/* this is valid value */
        }
    }
  if ( id != 0x0a ) return -ENODEV;	/* no valid device found */
  if ( codec -> hardware == CS4231_HW_DETECT ) {
    rev = snd_cs4231_in( codec, CS4231_VERSION ) & 0xe7;
    switch ( rev ) {
      case 0x80:
        codec -> hardware = CS4231_HW_CS4231;
        strcpy( pcm -> name, "CS4231" );
        break;
      case 0xa0:
        codec -> hardware = CS4231_HW_CS4231A;
        strcpy( pcm -> name, "CS4231A" );
        break;
      default:
        return -ENODEV;			/* unknown CS4231 chip? */
    }
  }

  snd_spin_lock( codec, reg, &flags );
  inb( CS4231P( codec, STATUS ) );	/* clear any pendings IRQ */
  outb( 0, CS4231P( codec, STATUS ) );
  mb();
  snd_spin_unlock( codec, reg, &flags );
  
#if 1
  codec -> image.mi = codec -> hardware == CS4231_HW_INTERWAVE ?
  				           CS4231_MODE3 : CS4231_MODE2;
#else
  codec -> image.mi = CS4231_MODE2;
#endif
  
  codec -> image.ic = 
    ( codec -> image.ic & ~CS4231_SINGLE_DMA ) | 
    ( codec -> single_dma ? CS4231_SINGLE_DMA : 0 );
  codec -> image.afei = 0x80;
  codec -> image.afeii = codec -> hardware == CS4231_HW_INTERWAVE ? 0xc2 : 0x01;
  ptr = (unsigned char *)&codec -> image;
  snd_cs4231_mce_down( codec );
  snd_spin_lock( codec, reg, &flags );
  for ( i = 0; i < 32; i++ )	/* ok.. fill all CS4231 registers */
    snd_cs4231_out( codec, i, *ptr++ );
  snd_spin_unlock( codec, reg, &flags );
  snd_cs4231_mce_up( codec );
  snd_cs4231_mce_down( codec );
  return 0;			/* all things are ok.. */
}

/*
 *
 */

static struct snd_stru_pcm_hardware snd_cs4231_playback = {
  NULL,				/* private data */
  NULL,				/* private_free */
  SND_PCM_HW_AUTODMA,		/* flags */
  SND_PCM_FMT_MU_LAW | SND_PCM_FMT_A_LAW | SND_PCM_FMT_IMA_ADPCM |
  SND_PCM_FMT_U8 | SND_PCM_FMT_S16_LE | SND_PCM_FMT_S16_BE,	/* formats */
  0,				/* align value */
  6,				/* minimal fragment */
  5510,				/* min. rate */
  48000,			/* max. rate */
  2,				/* max. voices */
  snd_cs4231_playback_open,
  snd_cs4231_playback_close,
  snd_cs4231_playback_compute_rate,
  snd_cs4231_playback_prepare,
  snd_cs4231_playback_trigger,
  snd_cs4231_playback_pointer,
  snd_cs4231_playback_dma,
  snd_pcm_dma_move,
  snd_pcm_playback_dma_neutral
};

static struct snd_stru_pcm_hardware snd_cs4231_record = {
  NULL,				/* private data */
  NULL,				/* private free */
  SND_PCM_HW_AUTODMA,		/* flags */
  SND_PCM_FMT_MU_LAW | SND_PCM_FMT_A_LAW | SND_PCM_FMT_IMA_ADPCM |
  SND_PCM_FMT_U8 | SND_PCM_FMT_S16_LE | SND_PCM_FMT_S16_BE,	/* formats */
  0,				/* align value */
  6,				/* minimal fragment */
  5510,				/* min. rate */
  48000,			/* max. rate */
  2,				/* max. voices */
  snd_cs4231_record_open,
  snd_cs4231_record_close,
  snd_cs4231_record_compute_rate,
  snd_cs4231_record_prepare,
  snd_cs4231_record_trigger,
  snd_cs4231_record_pointer,
  snd_pcm_record_dma,
  snd_pcm_dma_move,
  NULL
};

static void snd_cs4231_free( void *private_data )
{
  snd_free( private_data, sizeof( cs4231_t ) );
}

snd_pcm_t *snd_cs4231_new_device( snd_card_t *card,
                                  unsigned short port,
				  unsigned short irqnum,
			       	  unsigned short dmanum1,
				  unsigned short dmanum2,
				  unsigned short hardware )
{
  snd_pcm_t *pcm;
  cs4231_t *codec;
  char *str;
 
  pcm = snd_pcm_new_device( card, "CS4231" );
  if ( !pcm ) return NULL;
  codec = (cs4231_t *)snd_malloc( sizeof( cs4231_t ) );
  if ( !codec ) return NULL;
  memset( codec, 0, sizeof( cs4231_t ) );
  snd_spin_prepare( codec, reg );
  snd_spin_prepare( codec, mce );
  codec -> pcm = pcm;
  codec -> card = pcm -> card;
  codec -> port = port;
  codec -> irq = pcm -> card -> irqs[ irqnum ] -> irq;
  codec -> irqnum = irqnum;
  codec -> dmanum1 = dmanum1;
  codec -> dma1 = pcm -> card -> dmas[ dmanum1 ] -> dma;
  codec -> dmanum2 = dmanum2;
  codec -> dma2 = pcm -> card -> dmas[ dmanum2 ] -> dma;
  if ( codec -> dma1 == codec -> dma2 )
    codec -> single_dma = 1;
  codec -> hardware = hardware;
  memcpy( &codec -> image, &snd_cs4231_original_image, sizeof( snd_cs4231_original_image ) );
  memcpy( &pcm -> playback.hw, &snd_cs4231_playback, sizeof( snd_cs4231_playback ) );
  memcpy( &pcm -> record.hw, &snd_cs4231_record, sizeof( snd_cs4231_record ) );
  pcm -> private_data = codec;
  pcm -> private_free = snd_cs4231_free;
  pcm -> info_flags = SND_PCM_INFO_CODEC | SND_PCM_INFO_MMAP |
                      SND_PCM_INFO_PLAYBACK | SND_PCM_INFO_RECORD;
  if ( !codec -> single_dma )
    pcm -> info_flags |= SND_PCM_INFO_DUPLEX;
  if ( codec -> hardware != CS4231_HW_INTERWAVE )
    pcm -> info_flags |= SND_PCM_INFO_DUPLEX_LIMIT;
  switch ( codec -> hardware ) {
    case CS4231_HW_CS4231: str = "CS4231"; break;
    case CS4231_HW_CS4231A: str = "CS4231A"; break;
    case CS4231_HW_INTERWAVE: str = "AMD InterWave"; break;
    default: str = "???";
  }
  strcpy( pcm -> name, str );
  if ( snd_cs4231_probe( pcm ) < 0 ) {
    snd_pcm_free( pcm );
    return NULL;
  }
  return pcm;
}

/*
 *  MIXER part
 */

/*
 *    private_value
 *		0x000000ff - mute mask
 *		0xff000000 - left register (or mono register)
 *              0x00ff0000 - right register
 *              0x00000f00 - shift (from right)
 *              0x00001000 - invert mute
 *              0x00002000 - invert value
 */

static void snd_cs4231_record_source( snd_kmixer_t *mixer, snd_kmixer_channel_t *channel, int enable )
{  
  unsigned long flags;
  unsigned char mixs = CS4231_MIXS_LINE, mask = 0;
  cs4231_t *codec;
  
  switch ( channel -> hw.priority ) {
    case SND_MIXER_PRI_AUXA:	mask = 1; break;
    case SND_MIXER_PRI_MIC:	mask = 2; break;
    case SND_MIXER_PRI_LINE:	mask = 4; break;
    default: 			mask = 8; break;	/* master */
  }
  if ( enable )
    mixer -> private_value |= mask;
   else
    mixer -> private_value &= ~mask;

  if ( mixer -> private_value == 0 ) mixs = CS4231_MIXS_LINE; else
  if ( mixer -> private_value & 8 ) mixs = CS4231_MIXS_ALL; else
  if ( mixer -> private_value & 2 ) mixs = CS4231_MIXS_MIC; else
  if ( mixer -> private_value & 4 ) mixs = CS4231_MIXS_LINE; else
  mixs = CS4231_MIXS_AUX1;
        
  codec = (cs4231_t *)mixer -> private_data;
  snd_spin_lock( codec, reg, &flags );
  snd_cs4231_outm( codec, CS4231_LEFT_INPUT, 0x3f, mixs );
  snd_cs4231_outm( codec, CS4231_RIGHT_INPUT, 0x3f, mixs );
  snd_spin_unlock( codec, reg, &flags );
}

static void snd_cs4231_mute( snd_kmixer_t *mixer, snd_kmixer_channel_t *channel, unsigned int mute )
{
  unsigned long flags;
  unsigned char regl, regr, mask;
  cs4231_t *codec;

  regl = (unsigned char)(channel -> hw.private_value >> 24);
  regr = (unsigned char)(channel -> hw.private_value >> 16);
  mask = (unsigned char)(channel -> hw.private_value);
  if ( channel -> hw.private_value & 0x1000 ) mute ^= SND_MIX_MUTE;
#if 0
  snd_printk( "mute: regl = 0x%x(0x%x), regr = 0x%x(0x%x), mask = 0x%x, mute = 0x%x\n", regl, snd_cs4231_in( mixer -> private_data, regl ), regr, snd_cs4231_in( mixer -> private_data, regr ), mask, mute );
#endif
  codec = (cs4231_t *)mixer -> private_data;
  snd_spin_lock( codec, reg, &flags );
  snd_cs4231_outm( codec, regl, ~mask, (mute & SND_MIX_MUTE_LEFT) ? mask : 0 );
  if ( channel -> hw.stereo )
    snd_cs4231_outm( codec, regr, ~mask, (mute & SND_MIX_MUTE_RIGHT) ? mask : 0 );
  snd_spin_unlock( codec, reg, &flags );
}

static void snd_cs4231_volume_level( snd_kmixer_t *mixer, snd_kmixer_channel_t *channel, int left, int right )
{
  unsigned long flags;
  unsigned char regl, regr, shift, mask;
  cs4231_t *codec;

  regl = (unsigned char)(channel -> hw.private_value >> 24);
  regr = (unsigned char)(channel -> hw.private_value >> 16);
  shift = (channel -> hw.private_value >> 8) & 0x0f;
  mask = ~(unsigned char)(channel -> hw.max << shift);
#if 0
  snd_printk( "volume: regl = 0x%x(0x%x), regr = 0x%x(0x%x), mask = 0x%x, shift = %i, left = %i, right = %i\n", regl, snd_cs4231_in( mixer -> private_data, regl ), regr, snd_cs4231_in( mixer -> private_data, regr ), mask, shift, left, right );
#endif
  if ( !(channel -> hw.private_value & 0x2000) ) {
    left = channel -> hw.max - left;
    right = channel -> hw.max - right;
  }
  codec = (cs4231_t *)mixer -> private_data;
  snd_spin_lock( codec, reg, &flags );
  snd_cs4231_outm( codec, regl, mask, left << shift );
  if ( channel -> hw.stereo )
    snd_cs4231_outm( codec, regr, mask, right << shift );
  snd_spin_unlock( codec, reg, &flags );
}

#define CS4231_MIXS (sizeof(snd_cs4231_mixs)/sizeof(struct snd_stru_mixer_channel_hw))
#define CS4231_PRIVATE( left, right, shift, mute ) ((left << 24)|(right << 16)|(shift<<8)|mute)

static struct snd_stru_mixer_channel_hw snd_cs4231_mixs[] = {
  {
    SND_MIXER_PRI_GAIN,		/* priority */
    SND_MIXER_PRI_PARENT,	/* parent priority */
    SND_MIXER_ID_GAIN,		/* device name */
    SND_MIXER_OSS_IMIX,		/* OSS device # */
    0, 1, 0, 0, 1,		/* mute/stereo/record/digital/input */
    0, 15,			/* min, max value */
    0, 2250, 150,		/* min, max, step - dB */
    CS4231_PRIVATE( CS4231_LEFT_INPUT, CS4231_RIGHT_INPUT, 0, 0x00 ) | 0x2000,
    NULL,			/* compute dB -> linear */
    NULL,			/* compute linear -> dB */
    NULL,			/* record source */
    NULL,			/* set mute */
    snd_cs4231_volume_level,	/* set volume level */
  },
  {
    SND_MIXER_PRI_AUXA,		/* priority */
    SND_MIXER_PRI_PARENT,	/* parent priority */
    SND_MIXER_ID_AUXA,		/* device name */
    SND_MIXER_OSS_LINE1,	/* OSS device # */
    1, 1, 1, 0, 0,		/* mute/stereo/record/digital/input */
    0, 31,			/* min, max value */
    -3450, 1200, 150,		/* min, max, step - dB */
    CS4231_PRIVATE( CS4231_AUX1_LEFT_INPUT, CS4231_AUX1_RIGHT_INPUT, 0, 0x80 ),
    NULL,			/* compute dB -> linear */
    NULL,			/* compute linear -> dB */
    snd_cs4231_record_source,	/* record source */
    snd_cs4231_mute,		/* set mute */
    snd_cs4231_volume_level,	/* set volume level */
  },
  {
    SND_MIXER_PRI_AUXB,		/* priority */
    SND_MIXER_PRI_PARENT,	/* parent priority */
    SND_MIXER_ID_AUXB,		/* device name */
    SND_MIXER_OSS_LINE2,	/* OSS device # */
    1, 1, 0, 0,	0,		/* mute/stereo/record/digital/input */
    0, 31,			/* min, max value */
    -3450, 1200, 150,		/* min, max, step - dB */
    CS4231_PRIVATE( CS4231_AUX2_LEFT_INPUT, CS4231_AUX2_RIGHT_INPUT, 0, 0x80 ),
    NULL,			/* compute dB -> linear */
    NULL,			/* compute linear -> dB */
    snd_cs4231_record_source,	/* record source */
    snd_cs4231_mute,		/* set mute */
    snd_cs4231_volume_level,	/* set volume level */
  },
  {
    SND_MIXER_PRI_PCM,		/* priority */
    SND_MIXER_PRI_PARENT,	/* parent priority */
    SND_MIXER_ID_PCM,		/* device name */
    SND_MIXER_OSS_PCM,		/* OSS device # */
    1, 1, 0, 0, 0,		/* mute/stereo/record/digital/input */
    0, 63,			/* min, max value */
    -9450, 0, 150,		/* min, max, step - dB */
    CS4231_PRIVATE( CS4231_LEFT_OUTPUT, CS4231_RIGHT_OUTPUT, 0, 0x80 ),
    NULL,			/* compute dB -> linear */
    NULL,			/* compute linear -> dB */
    NULL,			/* record source */
    snd_cs4231_mute,		/* set mute */
    snd_cs4231_volume_level,	/* set volume level */
  },
  {
    SND_MIXER_PRI_LOOPBACK,	/* priority */
    SND_MIXER_PRI_PARENT,	/* parent priority */
    SND_MIXER_ID_LOOPBACK,	/* device name */
    SND_MIXER_OSS_UNKNOWN,	/* OSS device # */
    1, 0, 0, 0, 1,		/* mute/stereo/record/digital/input */
    0, 63,			/* min, max value */
    -9450, 0, 150,		/* min, max, step - dB */
    CS4231_PRIVATE( CS4231_LOOPBACK, 0, 0x12, 0x01 ),
    NULL,			/* compute dB -> linear */
    NULL,			/* compute linear -> dB */
    NULL,			/* record source */
    snd_cs4231_mute,		/* set mute */
    snd_cs4231_volume_level,	/* set volume level */
  },
  {
    SND_MIXER_PRI_LINE,		/* priority */
    SND_MIXER_PRI_PARENT,	/* parent priority */
    SND_MIXER_ID_LINE,		/* device name */
    SND_MIXER_OSS_LINE,		/* OSS device # */
    1, 1, 1, 0,	1,		/* mute/stereo/record/digital/input */
    0, 31,			/* min, max value */
    -3450, 1200, 150, 		/* min, max, step - dB */
    CS4231_PRIVATE( CS4231_LEFT_LINE_IN, CS4231_RIGHT_LINE_IN, 0, 0x80 ),
    NULL,			/* compute dB -> linear */
    NULL,			/* compute linear -> dB */
    snd_cs4231_record_source,	/* record source */
    snd_cs4231_mute,		/* set mute */
    snd_cs4231_volume_level,	/* set volume level */
  },
  {
    SND_MIXER_PRI_MIC,		/* priority */
    SND_MIXER_PRI_PARENT,	/* parent priority */
    SND_MIXER_ID_MIC,		/* device name */
    SND_MIXER_OSS_MIC,		/* OSS device # */
    1, 0, 1, 0, 1,		/* mute/stereo/record/digital/input */
    0, 15,			/* min, max value */
    -4500, 0, 300,		/* min, max, step - dB */
    CS4231_PRIVATE( CS4231_MONO_CTRL, 0, 0, 0x80 ),
    NULL,			/* compute dB -> linear */
    NULL,			/* compute linear -> dB */
    snd_cs4231_record_source,	/* record source */
    snd_cs4231_mute,		/* set mute */
    snd_cs4231_volume_level,	/* set volume level */
  },
};

snd_kmixer_t *snd_cs4231_new_mixer( snd_pcm_t *pcm )
{
  int idx;
  cs4231_t *codec;
  snd_kmixer_t *mixer;
  snd_kmixer_channel_t *channel;
  
  if ( !pcm || !pcm -> card ) return NULL;
  codec = (cs4231_t *)pcm -> private_data;
  if ( !codec ) return NULL;
  mixer = snd_mixer_new( pcm -> card, pcm -> id );
  if ( !mixer ) return NULL;
  strcpy( mixer -> name, pcm -> name );
  for ( idx = 0; idx < CS4231_MIXS; idx++ ) {
    channel = snd_mixer_new_channel( mixer, &snd_cs4231_mixs[ idx ] );
    if ( !channel ) {
      snd_mixer_free( mixer );
      return NULL;
    }
  }
  mixer -> hw.caps = SND_MIXER_INFO_CAP_EXCL_RECORD;
  mixer -> private_data = codec;
  codec -> mixer = mixer;
  return mixer;
}

/*
 *  INIT part
 */

#ifndef LINUX_2_1
extern struct symbol_table snd_symbol_table_cs4231_export;
#endif 

int init_module( void )
{
#ifndef LINUX_2_1
  if ( register_symtab( &snd_symbol_table_cs4231_export ) < 0 ) 
    return -ENOMEM;
#endif
  return 0;
}

void cleanup_module( void )
{
}
