/*
 *  Copyright (c) by Jaroslav Kysela (Perex soft)
 *  Routines for control of AD1848/AD1847/CS4248
 *
 *
 *   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 "ad1848.h"
#include "ulaw.h"

#if 0
#define SND_DEBUG_MCE
#endif

/*
 *  Some variables
 */

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

static struct snd_stru_ad1848_image snd_ad1848_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 - dfr */
  AD1848_AUTOCALIB,				/* 09 - ic */
  0x00,						/* 0a - pc */
  0x00,						/* 0b - ti */
  0x00,						/* 0c - mi */
  0x00,						/* 0d - lbc */
  0x00,						/* 0e - dru */
  0x00,						/* 0f - drl */
};

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

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

static void snd_ad1848_out( ad1848_t *codec, unsigned char reg, unsigned char value )
{
  int timeout;

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

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

#ifdef SNDCFG_DEBUG

void snd_ad1848_debug( ad1848_t *codec )
{
  printk( "AD1848 REGS:      INDEX = 0x%02x  ", inb( AD1848P( codec, REGSEL ) ) );
  printk( "                 STATUS = 0x%02x\n", inb( AD1848P( codec, STATUS ) ) );
  printk( "  0x00: left input      = 0x%02x  ", snd_ad1848_in( codec, 0x00 ) );
  printk( "  0x08: playback format = 0x%02x\n", snd_ad1848_in( codec, 0x08 ) );
  printk( "  0x01: right input     = 0x%02x  ", snd_ad1848_in( codec, 0x01 ) );
  printk( "  0x09: iface (CFIG 1)  = 0x%02x\n", snd_ad1848_in( codec, 0x09 ) );
  printk( "  0x02: AUXA left       = 0x%02x  ", snd_ad1848_in( codec, 0x02 ) );
  printk( "  0x0a: pin control     = 0x%02x\n", snd_ad1848_in( codec, 0x0a ) );
  printk( "  0x03: AUXA right      = 0x%02x  ", snd_ad1848_in( codec, 0x03 ) );
  printk( "  0x0b: init & status   = 0x%02x\n", snd_ad1848_in( codec, 0x0b ) );
  printk( "  0x04: AUXB left       = 0x%02x  ", snd_ad1848_in( codec, 0x04 ) );
  printk( "  0x0c: revision & mode = 0x%02x\n", snd_ad1848_in( codec, 0x0c ) );
  printk( "  0x05: AUXB right      = 0x%02x  ", snd_ad1848_in( codec, 0x05 ) );
  printk( "  0x0d: loopback        = 0x%02x\n", snd_ad1848_in( codec, 0x0d ) );
  printk( "  0x06: left output     = 0x%02x  ", snd_ad1848_in( codec, 0x06 ) );
  printk( "  0x0e: data upr count  = 0x%02x\n", snd_ad1848_in( codec, 0x0e ) );
  printk( "  0x07: right output    = 0x%02x  ", snd_ad1848_in( codec, 0x07 ) );
  printk( "  0x0f: data lwr count  = 0x%02x\n", snd_ad1848_in( codec, 0x0f ) );
}

#endif

/*
 *  AD1848 detection / MCE routines
 */
  
static void snd_ad1848_mce_up( ad1848_t *codec )
{
  unsigned long flags;
  int timeout;
  
  for ( timeout = 250; timeout > 0 && (inb( AD1848P( codec, REGSEL ) ) & AD1848_INIT); timeout-- )
    snd_delay( 1 );
#ifdef SNDCFG_DEBUG
  if ( inb( AD1848P( codec, REGSEL ) ) & AD1848_INIT )
    snd_printk( "ad1848_mce_up - auto calibration time out (0)\n" );
#endif
  snd_spin_lock( codec, mce, &flags );
  codec -> mce_bit |= AD1848_MCE;
  timeout = inb( AD1848P( codec, REGSEL ) );
  if ( timeout == 0x80 )
    snd_printk( "snd_ad1848_up [0x%x]: serious init problem - codec still busy\n", codec -> port );
  if ( !(timeout & AD1848_MCE) )
    outb( codec -> mce_bit | ( timeout & 0x1f ), AD1848P( codec, REGSEL ) );
  snd_spin_unlock( codec, mce, &flags );
}
  
static void snd_ad1848_mce_down( ad1848_t *codec )
{
  unsigned long flags;
  int timeout;
  unsigned long time;

  snd_spin_lock( codec, mce, &flags );
  for ( timeout = 5; timeout > 0; timeout-- )
    inb( AD1848P( codec, REGSEL ) );
  /* end of cleanup sequence */
  for ( timeout = 12000; timeout > 0 && (inb( AD1848P( codec, REGSEL ) ) & AD1848_INIT); timeout-- )
    snd_delay( 1 );
#if 0
  printk( "(1) timeout = %i\n", timeout );
#endif
#ifdef SNDCFG_DEBUG
  if ( inb( AD1848P( codec, REGSEL ) ) & AD1848_INIT )
    snd_printk( "ad1848_mce_down [0x%x] - auto calibration time out (0)\n", AD1848P( codec, REGSEL ) );
#endif
  codec -> mce_bit &= ~AD1848_MCE;
  timeout = inb( AD1848P( codec, REGSEL ) );
  outb( codec -> mce_bit | ( timeout & 0x1f ), AD1848P( codec, REGSEL ) );
  if ( timeout == 0x80 )
    snd_printk( "snd_ad1848_down [0x%x]: serious init problem - codec still busy\n", codec -> port );
  if ( (timeout & AD1848_MCE) == 0 ) {
    snd_spin_unlock( codec, mce, &flags );
    return;
  }

  /* calibration process */

  for ( timeout = 500; timeout > 0 && (snd_ad1848_in( codec, AD1848_TEST_INIT ) & AD1848_CALIB_IN_PROGRESS) == 0; timeout-- );
  if ( ( snd_ad1848_in( codec, AD1848_TEST_INIT ) & AD1848_CALIB_IN_PROGRESS ) == 0 ) {
    snd_printd( "ad1848_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_ad1848_in( codec, AD1848_TEST_INIT ) & AD1848_CALIB_IN_PROGRESS ) {
    snd_spin_unlock( codec, mce, &flags );
    if ( (signed long)(jiffies - time) >= 0 ) {
      snd_printk( "ad1848_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( AD1848P( codec, REGSEL ) ) & AD1848_INIT ) {
    snd_spin_unlock( codec, mce, &flags );
    if ( (signed long)(jiffies - time) >= 0 ) {
      snd_printk( "ad1848_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( "ad1848_mce_down - exit = 0x%x\n", inb( AD1848P( codec, REGSEL ) ) );
#endif
}

static unsigned int snd_ad1848_get_count( unsigned char format, unsigned int size )
{
  switch ( format & 0xe0 ) {
    case AD1848_LINEAR_16:
      size >>= 1;
      break;
  }
  if ( format & AD1848_STEREO ) size >>= 1;
  return size;
}

static void snd_ad1848_trigger( ad1848_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( AD1848P( card, STATUS ) ) );
#endif
  snd_spin_lock( codec, reg, &flags );
  if ( enable )
    {
      if ( codec -> image.ic & what ) {
        snd_spin_unlock( codec, reg, &flags );
        return;
      }
      snd_ad1848_out( codec, AD1848_IFACE_CTRL, codec -> image.ic |= what );
    }
   else
    {
      if ( !(codec -> image.ic & what) ) {
        snd_spin_unlock( codec, reg, &flags );
        return;
      }
      snd_ad1848_out( codec, AD1848_IFACE_CTRL, codec -> image.ic &= ~what );
    }
  snd_spin_unlock( codec, reg, &flags );
#if 0
  snd_ad1848_debug( codec );
  snd_delay( 100 );
  printk( "residue = 0x%x\n", get_dma_residue( codec -> dma ) );
#endif
}
 
/*
 *  CODEC I/O
 */

static unsigned char snd_ad1848_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_ad1848_freq[ i ].hertz )
      return snd_ad1848_freq[ i ].bits;
  return snd_ad1848_freq[ 13 ].bits;
}

static unsigned int snd_ad1848_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_ad1848_freq[ i ].hertz )
      return snd_ad1848_freq[ i ].rate;
  return snd_ad1848_freq[ 13 ].rate;
}

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

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

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

  format = 0;
  if ( mode & SND_PCM_MODE_16 )
    {
      format |= AD1848_LINEAR_16;
    }
   else
    {
      if ( mode & SND_PCM_MODE_ALAW )
        format |= AD1848_ALAW_8;
       else
      if ( mode & SND_PCM_MODE_ULAW )
        format |= AD1848_ULAW_8;
#if 0
       else
        format |= AD1848_LINEAR_8;	/* I know, OR with 0 */
#endif
    }
  if ( voices > 1 )
    format |= AD1848_STEREO;
#if 0
  snd_printk( "snd_ad1848_get_format: 0x%x (mode=0x%x)\n", format, mode );
#endif
  return format;
}

static void snd_ad1848_set_data_format( ad1848_t *codec, int direction )
{
  snd_pcm_channel_t *pchn;

  pchn = direction == SND_PCM_PLAYBACK ? &codec -> pcm -> playback : &codec -> pcm -> record;
  codec -> image.dfr = 
    snd_ad1848_get_format( pchn -> mode, pchn -> voices ) |
    snd_ad1848_get_freq( pchn -> real_rate );
#if 0
  snd_printk( ">>> pmode = 0x%x, dfr = 0x%x\n", pchn -> mode, codec -> image.dfr );
#endif
}
 
static void snd_ad1848_open( ad1848_t *codec )
{
  unsigned long flags;

  if ( codec -> mode & AD1848_MODE_OPEN ) return;

  snd_ad1848_mce_down( codec );

#ifdef SND_DEBUG_MCE
  snd_printk( "snd_ad1848_open: (1)\n" );
#endif
  snd_ad1848_mce_up( codec );
  snd_spin_lock( codec, reg, &flags );
  codec -> image.ic &= ~(AD1848_PLAYBACK_ENABLE|AD1848_PLAYBACK_PIO|
                         AD1848_RECORD_ENABLE|AD1848_RECORD_PIO|
                         AD1848_CALIB_MODE);
  codec -> image.ic |= AD1848_AUTOCALIB;
  snd_ad1848_out( codec, AD1848_IFACE_CTRL, codec -> image.ic );
  snd_spin_unlock( codec, reg, &flags );
  snd_ad1848_mce_down( codec );

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

  snd_ad1848_set_data_format( codec, SND_PCM_PLAYBACK );

  snd_ad1848_mce_up( codec );
  snd_spin_lock( codec, reg, &flags );
  snd_ad1848_out( codec, AD1848_DATA_FORMAT, codec -> image.dfr );
  snd_spin_unlock( codec, reg, &flags );
  snd_ad1848_mce_down( codec );

#ifdef SND_DEBUG_MCE
  snd_printk( "snd_ad1848_open: (3)\n" );
#endif

  /* ok. now enable and ack CODEC IRQ */
  snd_spin_lock( codec, reg, &flags );
  outb( 0, AD1848P( codec, STATUS ) );		/* clear IRQ */
  outb( 0, AD1848P( codec, STATUS ) );		/* clear IRQ */
  codec -> image.pc |= AD1848_IRQ_ENABLE;
  snd_ad1848_out( codec, AD1848_PIN_CTRL, codec -> image.pc );
  snd_spin_unlock( codec, reg, &flags );
}

static void snd_ad1848_close( ad1848_t *codec )
{
  unsigned long flags;

  if ( codec -> mode & AD1848_MODE_OPEN ) return;

  /* disable IRQ */
  snd_spin_lock( codec, reg, &flags );
  outb( 0, AD1848P( codec, STATUS ) );		/* clear IRQ */
  outb( 0, AD1848P( codec, STATUS ) );		/* clear IRQ */
  codec -> image.pc &= ~AD1848_IRQ_ENABLE;
  snd_ad1848_out( codec, AD1848_PIN_CTRL, codec -> image.pc );
  snd_spin_unlock( codec, reg, &flags );

  /* now disable record & playback */

  snd_ad1848_mce_up( codec );
  snd_spin_lock( codec, reg, &flags );
  codec -> image.ic &= ~(AD1848_PLAYBACK_ENABLE|AD1848_PLAYBACK_PIO|
                         AD1848_RECORD_ENABLE|AD1848_RECORD_PIO);
  snd_ad1848_out( codec, AD1848_IFACE_CTRL, codec -> image.ic );
  snd_spin_unlock( codec, reg, &flags );
  snd_ad1848_mce_down( codec );

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

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

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

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

  snd_ad1848_trigger( codec, AD1848_PLAYBACK_ENABLE, up );

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

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

static void snd_ad1848_record_trigger( snd_pcm_t *pcm, int up )
{
  ad1848_t *codec;
  
  codec = (ad1848_t *)pcm -> private_data;
  snd_ad1848_trigger( codec, AD1848_RECORD_ENABLE, up );
}

static void snd_ad1848_playback_prepare( snd_pcm_t *pcm,
                                         unsigned char *buffer,
                                         unsigned int size,
                                         unsigned int offset,
                                         unsigned int count )
{
  ad1848_t *codec;
  unsigned long flags;
  
  codec = (ad1848_t *)pcm -> private_data;
  /* Note: offset is always 0 if AUTO DMA */
  snd_ad1848_set_data_format( codec, SND_PCM_PLAYBACK );
#if 0
  snd_printk( "ad1848_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 &= ~(AD1848_PLAYBACK_ENABLE|AD1848_PLAYBACK_PIO);
  snd_dma_program( codec -> dma, buffer, size, DMA_MODE_WRITE | DMA_MODE_AUTOINIT );
  count = snd_ad1848_get_count( codec -> image.dfr, count ) - 1;
  snd_ad1848_mce_up( codec );
  snd_spin_lock( codec, reg, &flags );
  snd_ad1848_out( codec, AD1848_DATA_FORMAT, codec -> image.dfr );
#if 0
  printk( "ic = 0x%x, REC_FORMAT = 0x%x (0x%x), PLAYBK_FORMAT = 0x%x (0x%x)\n", codec -> image.ic, snd_ad1848_in( codec, AD1848_REC_FORMAT ), codec -> image.cdfr, snd_ad1848_in( codec, AD1848_PLAYBK_FORMAT ), codec -> image.pdfr );
#endif
  snd_spin_unlock( codec, reg, &flags );
  snd_ad1848_mce_down( codec );
  snd_spin_lock( codec, reg, &flags );
  snd_ad1848_out( codec, AD1848_DATA_LWR_CNT, (unsigned char)count );
  snd_ad1848_out( codec, AD1848_DATA_UPR_CNT, (unsigned char)(count >> 8) );
  snd_spin_unlock( codec, reg, &flags );
#if 0
  snd_ad1848_debug( codec );
#endif
}

static void snd_ad1848_record_prepare( snd_pcm_t *pcm,
					 unsigned char *buffer,
					 unsigned int size,
					 unsigned int offset,
					 unsigned int count )
{
  ad1848_t *codec;
  unsigned long flags;
  
  codec = (ad1848_t *)pcm -> private_data;
  snd_ad1848_set_data_format( codec, SND_PCM_RECORD );
#if 0
  snd_printk( "ad1848_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 &= ~(AD1848_RECORD_ENABLE|AD1848_RECORD_PIO);
  snd_dma_program( codec -> dma, buffer, size, DMA_MODE_READ | DMA_MODE_AUTOINIT );
  count = snd_ad1848_get_count( codec -> image.dfr, count ) - 1;
  snd_ad1848_mce_up( codec );
  snd_spin_lock( codec, reg, &flags );
  snd_ad1848_out( codec, AD1848_DATA_FORMAT, codec -> image.dfr );
#if 0
  printk( "ic = 0x%x, REC_FORMAT = 0x%x (0x%x), PLAYBK_FORMAT = 0x%x (0x%x)\n", codec -> image.ic, snd_ad1848_in( codec, AD1848_REC_FORMAT ), codec -> image.cdfr, snd_ad1848_in( codec, AD1848_PLAYBK_FORMAT ), codec -> image.pdfr );
#endif
  snd_spin_unlock( codec, reg, &flags );
  snd_ad1848_mce_down( codec );
#if 0
  printk( "cdfr = 0x%x, rport = 0x%x, pport = 0x%x, status = 0x%x\n", codec -> image.cdfr, snd_ad1848_in( codec, AD1848_REC_FORMAT ), snd_ad1848_in( codec, AD1848_PLAYBK_FORMAT ), inb( AD1848P( codec, REGSEL ) ) );
#endif
  snd_spin_lock( codec, reg, &flags );
  snd_ad1848_out( codec, AD1848_DATA_LWR_CNT, (unsigned char)count );
  snd_ad1848_out( codec, AD1848_DATA_UPR_CNT, (unsigned char)(count >> 8) );
  snd_spin_unlock( codec, reg, &flags );
}

void snd_ad1848_interrupt( snd_pcm_t *pcm, unsigned char status )
{
  unsigned long flags;
  ad1848_t *codec;

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

  if ( (codec -> mode & AD1848_MODE_PLAY) && pcm -> playback.ack )
    pcm -> playback.ack( pcm );
  if ( (codec -> mode & AD1848_MODE_RECORD) && pcm -> record.ack )
    pcm -> record.ack( pcm );
 
  snd_spin_lock( codec, reg, &flags );
  outb( 0, AD1848P( codec, STATUS ) );	/* clear global interrupt bit */
  snd_spin_unlock( codec, reg, &flags );
}

/*
 *
 */

static int snd_ad1848_playback_open( snd_pcm_t *pcm )
{
  snd_card_t *card;
  ad1848_t *codec;
  int err;

  card = pcm -> card;
  codec = (ad1848_t *)pcm -> private_data;
  if ( (err = snd_pcm_dma_alloc( pcm, SND_PCM_PLAYBACK, codec -> dmanum, "AD1848 (playback)" )) < 0 )
    return err;
  snd_ad1848_open( codec );
  codec -> mode |= AD1848_MODE_PLAY;
  return 0;
}

static int snd_ad1848_record_open( snd_pcm_t *pcm )
{
  snd_card_t *card;
  ad1848_t *codec;
  int err;

  card = pcm -> card;
  codec = (ad1848_t *)pcm -> private_data;
  if ( (err = snd_pcm_dma_alloc( pcm, SND_PCM_RECORD, codec -> dmanum, "AD1848 (record)" )) < 0 )
    return err;
  snd_ad1848_open( codec );
  codec -> mode |= AD1848_MODE_RECORD;
  return 0;
}

static void snd_ad1848_playback_close( snd_pcm_t *pcm )
{
  snd_card_t *card;
  ad1848_t *codec;

  card = pcm -> card;
  codec = (ad1848_t *)pcm -> private_data;
  codec -> mode &= ~AD1848_MODE_PLAY; 
  snd_ad1848_close( codec );
  snd_pcm_dma_free( pcm, SND_PCM_PLAYBACK, codec -> dmanum );
}

static void snd_ad1848_record_close( snd_pcm_t *pcm )
{
  snd_card_t *card;
  ad1848_t *codec;

  card = pcm -> card;
  codec = (ad1848_t *)pcm -> private_data;
  codec -> mode &= ~AD1848_MODE_RECORD;
  snd_ad1848_close( codec );
  snd_pcm_dma_free( pcm, SND_PCM_RECORD, codec -> dmanum );
}

static unsigned int snd_ad1848_playback_pointer( snd_pcm_t *pcm, unsigned int used_size )
{
  ad1848_t *codec;

  codec = (ad1848_t *)pcm -> private_data;
  if ( !(codec -> image.ic & AD1848_PLAYBACK_ENABLE) ) return 0;
  return used_size - snd_dma_residue( codec -> dma );
}

static unsigned int snd_ad1848_record_pointer( snd_pcm_t *pcm, unsigned int used_size )
{
  ad1848_t *codec;

  codec = (ad1848_t *)pcm -> private_data;
  if ( !(codec -> image.ic & AD1848_RECORD_ENABLE) ) return 0;
  return used_size - snd_dma_residue( codec -> dma );
}

/*
 *
 */

static int snd_ad1848_probe( snd_pcm_t *pcm )
{
  unsigned long flags;
  ad1848_t *codec;
  int i, id, rev, ad1847;
  unsigned char *ptr;

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

#if 0
  snd_ad1848_debug( codec );
#endif
  id = ad1847 = 0;
  for ( i = 0; i < 1000; i++ )
    {
      mb();
      if ( inb( AD1848P( codec, REGSEL ) ) & AD1848_INIT )
        snd_delay( 50 * 8 );
       else
        {
          snd_spin_lock( codec, reg, &flags );
          snd_ad1848_out( codec, AD1848_MISC_INFO, 0x00 );
          snd_ad1848_out( codec, AD1848_LEFT_INPUT, 0xaa );
          snd_ad1848_out( codec, AD1848_RIGHT_INPUT, 0x45 );
          rev = snd_ad1848_in( codec, AD1848_RIGHT_INPUT );
          if ( rev == 0x65 ) {
            id = 1;
            ad1847 = 1;
            break;
          }
          if ( snd_ad1848_in( codec, AD1848_LEFT_INPUT ) == 0xaa && rev == 0x45 ) {
            id = 1;
            break;
          }
        }
    }
  if ( id != 1 ) return -ENODEV;	/* no valid device found */
  if ( codec -> hardware == AD1848_HW_DETECT ) {
    if ( ad1847 ) {
      codec -> hardware = AD1848_HW_AD1847;
      strcpy( pcm -> name, "AD1847" );
    } else {
      codec -> hardware = AD1848_HW_AD1848;
      strcpy( pcm -> name, "AD1848" );
      rev = snd_ad1848_in( codec, AD1848_MISC_INFO );
      if ( rev & 0x80 ) {
        codec -> hardware = AD1848_HW_CS4248;
        strcpy( pcm -> name, "CS4248" );
      }
    }
  }

  snd_spin_lock( codec, reg, &flags );
  inb( AD1848P( codec, STATUS ) );	/* clear any pendings IRQ */
  outb( 0, AD1848P( codec, STATUS ) );
  mb();
  snd_spin_unlock( codec, reg, &flags );
  
  codec -> image.mi = 0x00;
  codec -> image.ic = 
    ( codec -> image.ic & ~AD1848_SINGLE_DMA ) | AD1848_SINGLE_DMA;
  ptr = (unsigned char *)&codec -> image;
  snd_ad1848_mce_down( codec );
  snd_spin_lock( codec, reg, &flags );
  for ( i = 0; i < 16; i++ )	/* ok.. fill all AD1848 registers */
    snd_ad1848_out( codec, i, *ptr++ );
  snd_spin_unlock( codec, reg, &flags );
  snd_ad1848_mce_up( codec );
  snd_ad1848_mce_down( codec );
  return 0;			/* all things are ok.. */
}

/*
 *
 */

static struct snd_stru_pcm_hardware snd_ad1848_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_U8 | SND_PCM_FMT_S16_LE,	/* formats */
  0,				/* align value */
  6,				/* minimal fragment */
  5510,				/* min. rate */
  48000,			/* max. rate */
  2,				/* max. voices */
  snd_ad1848_playback_open,
  snd_ad1848_playback_close,
  snd_ad1848_playback_compute_rate,
  snd_ad1848_playback_prepare,
  snd_ad1848_playback_trigger,
  snd_ad1848_playback_pointer,
  snd_pcm_playback_dma,
  snd_pcm_dma_move,
  snd_pcm_playback_dma_neutral
};

static struct snd_stru_pcm_hardware snd_ad1848_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_U8 | SND_PCM_FMT_S16_LE,	/* formats */
  0,				/* align value */
  6,				/* minimal fragment */
  5510,				/* min. rate */
  48000,			/* max. rate */
  2,				/* max. voices */
  snd_ad1848_record_open,
  snd_ad1848_record_close,
  snd_ad1848_record_compute_rate,
  snd_ad1848_record_prepare,
  snd_ad1848_record_trigger,
  snd_ad1848_record_pointer,
  snd_pcm_record_dma,
  snd_pcm_dma_move,
  NULL
};

static void snd_ad1848_free( void *private_data )
{
  snd_free( private_data, sizeof( ad1848_t ) );
}

snd_pcm_t *snd_ad1848_new_device( snd_card_t *card,
                                  unsigned short port,
				  unsigned short irqnum,
			       	  unsigned short dmanum,
				  unsigned short hardware )
{
  snd_pcm_t *pcm;
  ad1848_t *codec;
  char *str;
 
  pcm = snd_pcm_new_device( card, "AD1848" );
  if ( !pcm ) return NULL;
  codec = (ad1848_t *)snd_malloc( sizeof( ad1848_t ) );
  if ( !codec ) return NULL;
  memset( codec, 0, sizeof( ad1848_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 -> dmanum = dmanum;
  codec -> dma = pcm -> card -> dmas[ dmanum ] -> dma;
  codec -> hardware = hardware;
  memcpy( &codec -> image, &snd_ad1848_original_image, sizeof( snd_ad1848_original_image ) );
  memcpy( &pcm -> playback.hw, &snd_ad1848_playback, sizeof( snd_ad1848_playback ) );
  memcpy( &pcm -> record.hw, &snd_ad1848_record, sizeof( snd_ad1848_record ) );
  pcm -> private_data = codec;
  pcm -> private_free = snd_ad1848_free;
  pcm -> info_flags = SND_PCM_INFO_CODEC | SND_PCM_INFO_MMAP |
                      SND_PCM_INFO_PLAYBACK | SND_PCM_INFO_RECORD;
  switch ( codec -> hardware ) {
    case AD1848_HW_AD1847: str = "AD1847"; break;
    case AD1848_HW_AD1848: str = "AD1848"; break;
    case AD1848_HW_CS4248: str = "CS4248"; break;
    default: str = "???";
  }
  strcpy( pcm -> name, str );
  if ( snd_ad1848_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_ad1848_record_source( snd_kmixer_t *mixer, snd_kmixer_channel_t *channel, int enable )
{  
  unsigned long flags;
  unsigned char mixs = AD1848_MIXS_LINE, mask = 0;
  ad1848_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 = AD1848_MIXS_LINE; else
  if ( mixer -> private_value & 8 ) mixs = AD1848_MIXS_ALL; else
  if ( mixer -> private_value & 2 ) mixs = AD1848_MIXS_MIC; else
  if ( mixer -> private_value & 4 ) mixs = AD1848_MIXS_LINE; else
  mixs = AD1848_MIXS_AUX1;
        
  codec = (ad1848_t *)mixer -> private_data;
  snd_spin_lock( codec, reg, &flags );
  snd_ad1848_outm( codec, AD1848_LEFT_INPUT, 0x3f, mixs );
  snd_ad1848_outm( codec, AD1848_RIGHT_INPUT, 0x3f, mixs );
  snd_spin_unlock( codec, reg, &flags );
}

static void snd_ad1848_mute( snd_kmixer_t *mixer, snd_kmixer_channel_t *channel, unsigned int mute )
{
  unsigned long flags;
  unsigned char regl, regr, mask;
  ad1848_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_ad1848_in( mixer -> private_data, regl ), regr, snd_ad1848_in( mixer -> private_data, regr ), mask, mute );
#endif
  codec = (ad1848_t *)mixer -> private_data;
  snd_spin_lock( codec, reg, &flags );
  snd_ad1848_outm( codec, regl, ~mask, (mute & SND_MIX_MUTE_LEFT) ? mask : 0 );
  if ( channel -> hw.stereo )
    snd_ad1848_outm( codec, regr, ~mask, (mute & SND_MIX_MUTE_RIGHT) ? mask : 0 );
  snd_spin_unlock( codec, reg, &flags );
}

static void snd_ad1848_volume_level( snd_kmixer_t *mixer, snd_kmixer_channel_t *channel, int left, int right )
{
  unsigned long flags;
  unsigned char regl, regr, shift, mask;
  ad1848_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_ad1848_in( mixer -> private_data, regl ), regr, snd_ad1848_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 = (ad1848_t *)mixer -> private_data;
  snd_spin_lock( codec, reg, &flags );
  snd_ad1848_outm( codec, regl, mask, left << shift );
  if ( channel -> hw.stereo )
    snd_ad1848_outm( codec, regr, mask, right << shift );
  snd_spin_unlock( codec, reg, &flags );
}

#define AD1848_MIXS (sizeof(snd_ad1848_mixs)/sizeof(struct snd_stru_mixer_channel_hw))
#define AD1848_PRIVATE( left, right, shift, mute ) ((left << 24)|(right << 16)|(shift<<8)|mute)

static struct snd_stru_mixer_channel_hw snd_ad1848_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 */
    AD1848_PRIVATE( AD1848_LEFT_INPUT, AD1848_RIGHT_INPUT, 0, 0x00 ) | 0x2000,
    NULL,			/* compute dB -> linear */
    NULL,			/* compute linear -> dB */
    NULL,			/* record source */
    NULL,			/* set mute */
    snd_ad1848_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 */
    AD1848_PRIVATE( AD1848_AUX1_LEFT_INPUT, AD1848_AUX1_RIGHT_INPUT, 0, 0x80 ),
    NULL,			/* compute dB -> linear */
    NULL,			/* compute linear -> dB */
    snd_ad1848_record_source,	/* record source */
    snd_ad1848_mute,		/* set mute */
    snd_ad1848_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 */
    AD1848_PRIVATE( AD1848_AUX2_LEFT_INPUT, AD1848_AUX2_RIGHT_INPUT, 0, 0x80 ),
    NULL,			/* compute dB -> linear */
    NULL,			/* compute linear -> dB */
    snd_ad1848_record_source,	/* record source */
    snd_ad1848_mute,		/* set mute */
    snd_ad1848_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 */
    AD1848_PRIVATE( AD1848_LEFT_OUTPUT, AD1848_RIGHT_OUTPUT, 0, 0x80 ),
    NULL,			/* compute dB -> linear */
    NULL,			/* compute linear -> dB */
    NULL,			/* record source */
    snd_ad1848_mute,		/* set mute */
    snd_ad1848_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 */
    AD1848_PRIVATE( AD1848_LOOPBACK, 0, 0x12, 0x01 ),
    NULL,			/* compute dB -> linear */
    NULL,			/* compute linear -> dB */
    NULL,			/* record source */
    snd_ad1848_mute,		/* set mute */
    snd_ad1848_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 # */
    0, 1, 1, 0, 1,		/* mute/stereo/record/digital/input */
    0, 1,			/* min, max value */
    -4500, 0, 300,		/* min, max, step - dB */
    AD1848_PRIVATE( AD1848_LEFT_INPUT, AD1848_RIGHT_INPUT, 5, 0 ) | 0x2000,
    NULL,			/* compute dB -> linear */
    NULL,			/* compute linear -> dB */
    snd_ad1848_record_source,	/* record source */
    NULL,			/* set mute */
    snd_ad1848_volume_level,	/* set volume level */
  },
};

snd_kmixer_t *snd_ad1848_new_mixer( snd_pcm_t *pcm )
{
  int idx;
  ad1848_t *codec;
  snd_kmixer_t *mixer;
  snd_kmixer_channel_t *channel;
  
  if ( !pcm || !pcm -> card ) return NULL;
  codec = (ad1848_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 < AD1848_MIXS; idx++ ) {
    channel = snd_mixer_new_channel( mixer, &snd_ad1848_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_ad1848_export;
#endif 

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

void cleanup_module( void )
{
}
