/*
 *  Copyright (c) by Jaroslav Kysela (Perex soft)
 *  Routines for control of ESS ES1688/688/488 chip
 *
 *
 *   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 "es1688.h"

static int snd_es1688_dsp_command( es1688_t *codec, unsigned char val )
{
  int i;
  
  for ( i = 10000; i; i-- )
    if ( ( inb( ES1688P( codec, STATUS ) ) & 0x80 ) == 0 ) {
      outb( val, ES1688P( codec, COMMAND ) );
      return 1;
    }
#ifdef SNDCFG_DEBUG
  printk( "snd_es1688_dsp_command: timeout (0x%x)\n", val );
#endif
  return 0;
}

static int snd_es1688_dsp_get_byte( es1688_t *codec )
{
  int i;
  
  for ( i = 1000; i; i-- )
    if ( inb( ES1688P( codec, DATA_AVAIL ) ) & 0x80 )
      return inb( ES1688P( codec, READ ) );
  snd_printd( "ultra es1688 get byte failed: 0x%x = 0x%x!!!\n", ES1688P( codec, DATA_AVAIL ), inb( ES1688P( codec, DATA_AVAIL ) ) );
  return -ENODEV;
}

static int snd_es1688_write( es1688_t *codec, unsigned char reg, unsigned char data )
{
  if ( !snd_es1688_dsp_command( codec, reg ) ) return 0;
  return snd_es1688_dsp_command( codec, data );
}

int snd_es1688_read( es1688_t *codec, unsigned char reg )
{
  /* Read a byte from an extended mode register of ES1688 */  
  if ( !snd_es1688_dsp_command( codec, 0xc0 ) ) return -1;        
  if ( !snd_es1688_dsp_command( codec, reg ) ) return -1;
  return snd_es1688_dsp_get_byte( codec );
}

static void snd_es1688_mixer_write( es1688_t *codec, unsigned char reg, unsigned char data )
{
  outb( reg, ES1688P( codec, MIXER_ADDR ) );
  snd_delay( 1 );
  outb( data, ES1688P( codec, MIXER_DATA ) );
  snd_delay( 1 );
}

static unsigned char snd_es1688_mixer_read( es1688_t *codec, unsigned char reg )
{
  unsigned char result;
  
  outb( reg, ES1688P( codec, MIXER_ADDR ) );
  snd_delay( 1 );
  result = inb( ES1688P( codec, MIXER_DATA ) );
  snd_delay( 1 );
  return result;
}

static int snd_es1688_reset( es1688_t *codec )
{
  int i;

  outb( 3, ES1688P( codec, RESET ) );	/* valid only for ESS chips, SB -> 1 */
  snd_delay( 1 );
  outb( 0, ES1688P( codec, RESET ) );
  snd_delay( 3 );
  for ( i = 0; i < 1000 && !(inb( ES1688P( codec, DATA_AVAIL ) ) & 0x80); i++ );
  if ( inb( ES1688P( codec, READ ) ) != 0xaa ) {
#ifdef SNDCFG_DEBUG
    snd_printk( "ess_reset: failed!!!\n" );
#endif
    return -ENODEV;
  }
  snd_es1688_dsp_command( codec, 0xc6 );	/* enable extended mode */
  return 0;
}

static int snd_es1688_probe( snd_pcm_t *pcm )
{
  unsigned long flags;
  unsigned short major, minor, hw;
  es1688_t *codec;
  char *str;
  int i;

  /*
   *  initialization sequence
   */
  
  codec = (es1688_t *)pcm -> private_data;
  snd_spin_lock( codec, reg, &flags );	/* Some ESS1688 cards need this */
  inb( ES1688P( codec, ENABLE1 ) );
  inb( ES1688P( codec, ENABLE1 ) );
  inb( ES1688P( codec, ENABLE1 ) );
  inb( ES1688P( codec, ENABLE2 ) );
  inb( ES1688P( codec, ENABLE1 ) );
  inb( ES1688P( codec, ENABLE2 ) );
  inb( ES1688P( codec, ENABLE1 ) );
  inb( ES1688P( codec, ENABLE1 ) );
  inb( ES1688P( codec, ENABLE2 ) );
  inb( ES1688P( codec, ENABLE1 ) );  
  inb( ES1688P( codec, ENABLE0 ) );

  if ( snd_es1688_reset( codec ) < 0 ) {
    snd_printdd( "ESS: [0x%x] reset failed... 0x%x\n", codec -> port, inb( ES1688P( codec, READ ) ) );
    snd_spin_unlock( codec, reg, &flags );
    return -ENODEV;
  }

  snd_es1688_dsp_command( codec, 0xe7 ); 	/* return identification */
  
  for ( i = 1000, major = minor = 0; i; i-- ) {
    if ( inb( ES1688P( codec, DATA_AVAIL ) ) & 0x80 )
      if ( major == 0 )
        major = inb( ES1688P( codec, READ ) );
       else
        minor = inb( ES1688P( codec, READ ) );
  }

  snd_spin_unlock( codec, reg, &flags );

  snd_printdd( "ESS: [0x%x] found.. major = 0x%x, minor = 0x%x\n", codec -> port, major, minor );

  codec -> version = (major << 8) | minor;
  if ( !codec -> version ) return -ENODEV;	/* probably SB */
  
  hw = ES1688_HW_AUTO;
  switch ( codec -> version & 0xfff0 ) {
    case 0x4880:
      snd_printk( "[0x%x] ESS: AudioDrive ES488 detected, but driver is in another place\n", codec -> port );
      return -ENODEV;
    case 0x6880:
      hw = (codec -> version & 0x0f) >= 8 ? ES1688_HW_1688 : ES1688_HW_688;
      break;
    default:
      snd_printk( "[0x%x] ESS: unknown AudioDrive chip with version 0x%x (Jazz16 soundcard?)\n", codec -> port, codec -> version );
      return -ENODEV;
  }
  if ( codec -> hardware != ES1688_HW_AUTO ) {
    if ( codec -> hardware == ES1688_HW_1688_GUS ) {
      if ( hw != ES1688_HW_1688 ) {
        snd_printk( "[0x%x] ESS: oops, seems that different ESS chip with version 0x%x is detected for GUS Extreme!!!\n", codec -> port, codec -> version );
        return -ENODEV;
      }
      hw = ES1688_HW_1688_GUS;
    }
  }
  switch ( codec -> hardware = hw ) {
    case ES1688_HW_688: str = "ESS AudioDrive ES688"; break;
    case ES1688_HW_1688:
    case ES1688_HW_1688_GUS: str = "ESS AudioDrive ES1688"; break;
    default: str = "???";
  }
  strcpy( pcm -> name, str );

  snd_spin_lock( codec, reg, &flags );
  snd_es1688_write( codec, 0xb1, 0x10 );	/* disable IRQ */
  snd_es1688_write( codec, 0xb2, 0x00 );	/* disable DMA */
  snd_spin_unlock( codec, reg, &flags );

  if ( codec -> hardware == ES1688_HW_1688_GUS ) {
    int i, cfg1, cfg2, cfg3;

    /*
     *  check if we have GUS Extreme, don't try initialize other ESS cards
     */


    snd_spin_lock( codec, reg, &flags );
    i = inb( ES1688P( codec, INIT1 ) );
    snd_printdd( "ESS: [0x%x] (1) init1 = 0x%x\n", codec -> port, i );
    if ( !( i & 0x40 ) ) {
      snd_spin_unlock( codec, reg, &flags );
      return -ENODEV;
    }
    outb( 0, ES1688P( codec, INIT1 ) );
    i = inb( ES1688P( codec, INIT1 ) );
    snd_spin_unlock( codec, reg, &flags );
    snd_printdd( "ESS: [0x%x] (2) init1 = 0x%x\n", codec -> port, i );
    if ( i != 0x40 ) {
      return -ENODEV;
    }

    /*
     * This is main stuff - enable access to GF1 chip...
     * I'm not sure, if this will work for card which have
     * ES 1688 chip in another place than 0x220.
     *
     * ULTRINIT.EXE:
     * 0x230 = 0,2,3
     * 0x240 = 2,0,1
     * 0x250 = 2,0,3
     * 0x260 = 2,2,1
     */

    switch ( codec -> port ) {
      case 0x220:
      case 0x230:
        /* card -> gf1.port = 0x240; */
        cfg1 = 2; cfg2 = 0; cfg3 = 1;
        break;
      case 0x240:
      case 0x250:
        /* card -> gf1.port = 0x260; */
        cfg1 = 2; cfg2 = 2; cfg3 = 1;
        break;
      default:
        snd_printk( "GUS Extreme: ESS chip found at unknown port 0x%x!!!", codec -> port );
        snd_spin_unlock( codec, reg, &flags );
        return -EINVAL;
    }

    snd_spin_lock( codec, mixer, &flags );
    snd_es1688_mixer_write( codec, 0x40, 0x0b ); /* don't change!!!! */
    snd_spin_unlock( codec, mixer, &flags );
    snd_spin_lock( codec, reg, &flags );
    outb( cfg1, ES1688P( codec, INIT1 ) );	/* 0x230 = 0 */
    outb( 0, 0x201 );
    outb( cfg2, ES1688P( codec, INIT1 ) );	/* 0x230 = 2 */
    outb( 0, 0x201 );
    outb( cfg3, ES1688P( codec, INIT1 ) );	/* 0x230,0x250 = 3 */
    snd_spin_unlock( codec, reg, &flags );
  }

  /* enable joystick, but disable OPL3 */  
  snd_spin_lock( codec, mixer, &flags );
  snd_es1688_mixer_write( codec, 0x40, 0x01 );
  snd_spin_unlock( codec, mixer, &flags );

  return 0;
}

int snd_es1688_init( snd_pcm_t *pcm, int enable )
{
  static int irqs[ 16 ] = { -1, -1, 0, -1, -1, 1, -1, 2, -1, 0, 3, -1, -1, -1, -1, -1 };
  es1688_t *codec;
  unsigned long flags;
  int cfg, irq_bits, dma, dma_bits, tmp, tmp1;
  
  codec = (es1688_t *)pcm -> private_data;
  /* ok.. setup MPU-401 port and joystick and OPL3 */
  cfg = 0x01;		/* enable joystick, but disable OPL3 */
  if ( enable && codec -> mpu_port >= 0x300 && codec -> mpu_irq > 0 && codec -> hardware != ES1688_HW_688 ) {
    tmp = (codec -> mpu_port & 0x0f0) >> 4;
    if ( tmp <= 3 ) {
      switch ( codec -> mpu_irq ) {
        case 9: tmp1 = 4; break;
        case 5: tmp1 = 5; break;
        case 7: tmp1 = 6; break;
        case 10: tmp1 = 7; break;
        default: tmp1 = 0;
      }
      if ( tmp1 ) {
        cfg |= (tmp << 3) | (tmp1 << 5);
      }
    }
  }
#if 0
  snd_printk( "mpu cfg = 0x%x\n", cfg );
#endif
  snd_spin_lock( codec, reg, &flags );
  snd_es1688_mixer_write( codec, 0x40, cfg );
  snd_spin_unlock( codec, reg, &flags );
  /* --- */
  snd_spin_lock( codec, reg, &flags );
  snd_es1688_read( codec, 0xb1 );
  snd_es1688_read( codec, 0xb2 );
  snd_spin_unlock( codec, reg, &flags );
  if ( enable ) {
    cfg = 0xf0;		/* enable only DMA counter interrupt */
    irq_bits = irqs[ codec -> irq & 0x0f ];
    if ( irq_bits < 0 ) {
      snd_printk( "[0x%x] ESS: bad IRQ %d for ES1688 chip!!\n", codec -> port, codec -> irq );
#if 0
      irq_bits = 0;
      cfg = 0x10;
#endif
      return -EINVAL;
    }
    snd_spin_lock( codec, reg, &flags );
    snd_es1688_write( codec, 0xb1, cfg | (irq_bits << 2) );
    snd_spin_unlock( codec, reg, &flags );
    cfg = 0xf0;		/* extended mode DMA enable */
    dma = codec -> dma8;
    if ( dma > 3 || dma == 2 ) {
      snd_printk( "[0x%x] ESS: bad DMA channel %d for ES1688 chip!!\n", codec -> port, dma );
#if 0
      dma_bits = 0;
      cfg = 0x00;	/* disable all DMA */
#endif
      return -EINVAL;
    } else {
      dma_bits = dma;
      if ( dma != 3 ) dma_bits++;
    }
    snd_spin_lock( codec, reg, &flags );
    snd_es1688_write( codec, 0xb2, cfg | (dma_bits << 2) );
    snd_spin_unlock( codec, reg, &flags );
  } else {
#if 1
    snd_spin_lock( codec, reg, &flags );
    snd_es1688_write( codec, 0xb1, 0x10 );	/* disable IRQ */
    snd_es1688_write( codec, 0xb2, 0x00 );	/* disable DMA */
    snd_spin_unlock( codec, reg, &flags );
#endif
  }
  snd_spin_lock( codec, reg, &flags );
  snd_es1688_read( codec, 0xb1 );
  snd_es1688_read( codec, 0xb2 );
  snd_es1688_reset( codec );
  snd_spin_unlock( codec, reg, &flags );
  return 0;
}

/*
 *
 */

static void snd_es1688_set_rate( es1688_t *codec, int rate )
{
  int divider;
  unsigned char bits;

  if ( rate < 4000 ) rate = 4000;
  if ( rate > 48000 ) rate = 48000;
  if ( rate > 22000 ) {
    bits = 0x80;
    divider = 256 - (795500 + (rate >> 1)) / rate;
  } else {
    bits = 0x00;
    divider = 128 - (397700 + (rate >> 1)) / rate;
  }
  bits |= divider;
  snd_es1688_write( codec, 0xa1, bits );
  /* set filter register */
  rate = (rate * 9) / 20;
  divider = 256 - 7160000 / (rate * 82);
  snd_es1688_write( codec, 0xa2, divider );
}

static void snd_es1688_playback_compute_rate( snd_pcm_t *pcm )
{
  pcm -> playback.real_rate = pcm -> playback.rate;
  if ( pcm -> playback.real_rate < 4000 ) pcm -> playback.real_rate = 4000;
  if ( pcm -> playback.real_rate > 44100 ) pcm -> playback.real_rate = 44100;
}

static void snd_es1688_record_compute_rate( snd_pcm_t *pcm )
{
  pcm -> record.real_rate = pcm -> record.rate;
  if ( pcm -> record.real_rate < 4000 ) pcm -> record.real_rate = 4000;
  if ( pcm -> record.real_rate > 44100 ) pcm -> record.real_rate = 44100;
}

static void snd_es1688_trigger( es1688_t *codec, unsigned char value )
{
  unsigned long flags;
  int val;

  snd_spin_lock( codec, reg, &flags );
  codec -> trigger_value = value;
  val = snd_es1688_read( codec, 0xb8 );
  if ( (val < 0) || (val & 0x0f) == value ) {
    snd_spin_unlock( codec, reg, &flags );
    return;		/* something is wrong */
  }
#if 0
  printk( "trigger: val = 0x%x, value = 0x%x\n", val, value );
  printk( "trigger: residue = 0x%x\n", get_dma_residue( codec -> dma8 ) );
#endif
  snd_es1688_write( codec, 0xb8, (val & 0xf0) | value );
  snd_spin_unlock( codec, reg, &flags );
}

static void snd_es1688_playback_prepare( snd_pcm_t *pcm,
                                         unsigned char *buffer,
                                         unsigned int size,
                                         unsigned int offset,
                                         unsigned int count )
{
  unsigned long flags;
  es1688_t *codec;
  snd_pcm_channel_t *pchn;

  pchn = &pcm -> playback;
  codec = (es1688_t *)pcm -> private_data;
  snd_spin_lock( codec, reg, &flags );
  snd_es1688_reset( codec );
  snd_es1688_set_rate( codec, pchn -> rate );
  snd_es1688_write( codec, 0xb8, 4 );	/* auto init DMA mode */
  snd_es1688_write( codec, 0xa8, (snd_es1688_read( codec, 0xa8 ) & ~0x03) | (3 - pchn -> voices) );
  snd_es1688_write( codec, 0xb9, 2 );	/* demand mode (4 bytes/request) */
  if ( pchn -> voices == 1 ) {
    if ( !(pchn -> mode & SND_PCM_MODE_16) ) { 	 /* 8. bit mono */
      snd_es1688_write( codec, 0xb6, 0x80 );
      snd_es1688_write( codec, 0xb7, 0x51 );
      snd_es1688_write( codec, 0xb7, 0xd0 );
    } else {					 /* 16. bit stereo */
      snd_es1688_write( codec, 0xb6, 0x00 );
      snd_es1688_write( codec, 0xb7, 0x71 );
      snd_es1688_write( codec, 0xb7, 0xf4 );
    }
  } else {
    if ( !(pchn -> mode & SND_PCM_MODE_16) ) { 	 /* 8. bit stereo */
      snd_es1688_write( codec, 0xb6, 0x80 );
      snd_es1688_write( codec, 0xb7, 0x51 );
      snd_es1688_write( codec, 0xb7, 0x98 );
    } else {					 /* 16. bit stereo */
      snd_es1688_write( codec, 0xb6, 0x00 );
      snd_es1688_write( codec, 0xb7, 0x71 );
      snd_es1688_write( codec, 0xb7, 0xbc );
    }
  }
  snd_es1688_write( codec, 0xb1, (snd_es1688_read( codec, 0xb1 ) & 0x0f) | 0x50 );
  snd_es1688_write( codec, 0xb2, (snd_es1688_read( codec, 0xb2 ) & 0x0f) | 0x50 );
  snd_es1688_dsp_command( codec, ES1688_DSP_CMD_SPKON );
  snd_spin_unlock( codec, reg, &flags );
  /* --- */
  count = -count;
  snd_dma_program( codec -> dma8, buffer, size, DMA_MODE_WRITE | DMA_MODE_AUTOINIT );
  snd_spin_lock( codec, reg, &flags );
  snd_es1688_write( codec, 0xa4, (unsigned char)count );
  snd_es1688_write( codec, 0xa5, (unsigned char)(count >> 8) );
  snd_spin_unlock( codec, reg, &flags );
}

static void snd_es1688_playback_trigger( snd_pcm_t *pcm, int up )
{
  es1688_t *codec;

  codec = (es1688_t *)pcm -> private_data;
  snd_es1688_trigger( codec, up ? 0x05 : 0x00 );
}

static void snd_es1688_record_prepare( snd_pcm_t *pcm,
                                       unsigned char *buffer,
                                       unsigned int size,
                                       unsigned int offset,
                                       unsigned int count )
{
  unsigned long flags;
  es1688_t *codec;
  snd_pcm_channel_t *pchn;

  pchn = &pcm -> record;
  codec = (es1688_t *)pcm -> private_data;
  snd_spin_lock( codec, reg, &flags );
  snd_es1688_reset( codec );
  snd_es1688_set_rate( codec, pchn -> rate );
  snd_es1688_dsp_command( codec, ES1688_DSP_CMD_SPKOFF );
  snd_es1688_write( codec, 0xb8, 0x0e ); /* auto init DMA mode */
  snd_es1688_write( codec, 0xa8, (snd_es1688_read( codec, 0xa8 ) & ~0x03) | (3 - pchn -> voices) );
  snd_es1688_write( codec, 0xb9, 2 );	/* demand mode (4 bytes/request) */
  if ( pchn -> voices == 1 ) {
    if ( !(pchn -> mode & SND_PCM_MODE_16) ) { 	/* 8. bit mono */
      snd_es1688_write( codec, 0xb7, 0x51 );
      snd_es1688_write( codec, 0xb7, 0xd0 );
    } else {					/* 16. bit stereo */
      snd_es1688_write( codec, 0xb7, 0x71 );
      snd_es1688_write( codec, 0xb7, 0xf4 );
    }
  } else {
    if ( !(pchn -> mode & SND_PCM_MODE_16) ) {	/* 8. bit stereo */
      snd_es1688_write( codec, 0xb7, 0x51 );
      snd_es1688_write( codec, 0xb7, 0x98 );
    } else {					/* 16. bit stereo */
      snd_es1688_write( codec, 0xb7, 0x71 );
      snd_es1688_write( codec, 0xb7, 0xbc );
    }
  }
  snd_es1688_write( codec, 0xb1, (snd_es1688_read( codec, 0xb1 ) & 0x0f) | 0x50 );
  snd_es1688_write( codec, 0xb2, (snd_es1688_read( codec, 0xb2 ) & 0x0f) | 0x50 );
  snd_spin_unlock( codec, reg, &flags );
  /* --- */
  count = -count;
  snd_dma_program( codec -> dma8, buffer, size, DMA_MODE_READ | DMA_MODE_AUTOINIT );
  snd_spin_lock( codec, reg, &flags );
  snd_es1688_write( codec, 0xa4, (unsigned char)count );
  snd_es1688_write( codec, 0xa5, (unsigned char)(count >> 8) );
  snd_spin_unlock( codec, reg, &flags );
}

static void snd_es1688_record_trigger( snd_pcm_t *pcm, int up )
{
  es1688_t *codec;

  codec = (es1688_t *)pcm -> private_data;
  snd_es1688_trigger( codec, up ? 0x0f : 0x00 );
}

void snd_es1688_interrupt( snd_pcm_t *pcm )
{
  es1688_t *codec;
  
  codec = (es1688_t *)pcm -> private_data;
  if ( codec -> trigger_value == 0x05 ) 	/* ok.. playback is active */
    pcm -> playback.ack( pcm );
  if ( codec -> trigger_value == 0x0f )		/* ok.. record is active */
    pcm -> record.ack( pcm );

  inb( ES1688P( codec, DATA_AVAIL ) );		/* ack interrupt */
}

/*
 *
 */

static int snd_es1688_playback_open( snd_pcm_t *pcm )
{
  snd_card_t *card;
  es1688_t *codec;
  int err;

  card = pcm -> card;
  codec = (es1688_t *)pcm -> private_data;
  if ( (err = snd_pcm_dma_alloc( pcm, SND_PCM_PLAYBACK, codec -> dma8num, "ESx688 (playback)" )) < 0 )
    return err;
  return 0;
}

static int snd_es1688_record_open( snd_pcm_t *pcm )
{
  snd_card_t *card;
  es1688_t *codec;
  int err;

  card = pcm -> card;
  codec = (es1688_t *)pcm -> private_data;
  if ( (err = snd_pcm_dma_alloc( pcm, SND_PCM_RECORD, codec -> dma8num, "ESx688 (record)" )) < 0 )
    return err;
  return 0;
}

static void snd_es1688_playback_close( snd_pcm_t *pcm )
{
  es1688_t *codec;

  codec = (es1688_t *)pcm -> private_data;
  snd_pcm_dma_free( pcm, SND_PCM_PLAYBACK, codec -> dma8num );
}

static void snd_es1688_record_close( snd_pcm_t *pcm )
{
  es1688_t *codec;

  codec = (es1688_t *)pcm -> private_data;
  snd_pcm_dma_free( pcm, SND_PCM_RECORD, codec -> dma8num );
}

static unsigned int snd_es1688_playback_pointer( snd_pcm_t *pcm, unsigned int used_size )
{
  es1688_t *codec;

  codec = (es1688_t *)pcm -> private_data;
  if ( codec -> trigger_value != 0x05 ) return 0;
  return used_size - snd_dma_residue( codec -> dma8 );
}

static unsigned int snd_es1688_record_pointer( snd_pcm_t *pcm, unsigned int used_size )
{
  es1688_t *codec;

  codec = (es1688_t *)pcm -> private_data;
  if ( codec -> trigger_value != 0x0f ) return 0;
  return used_size - snd_dma_residue( codec -> dma8 );
}

/*
 *
 */

static void snd_es1688_free( void *private_data );

static struct snd_stru_pcm_hardware snd_es1688_playback = {
  NULL,				/* private data */
  NULL,				/* private_free */
  SND_PCM_HW_AUTODMA,		/* flags */
  SND_PCM_FMT_U8 | SND_PCM_FMT_S16_LE,	/* formats */
  0,				/* align value */
  6,				/* minimal fragment */
  4000,				/* min. rate */
  48000,			/* max. rate */
  2,				/* max. voices */
  snd_es1688_playback_open,
  snd_es1688_playback_close,
  snd_es1688_playback_compute_rate,
  snd_es1688_playback_prepare,
  snd_es1688_playback_trigger,
  snd_es1688_playback_pointer,
  snd_pcm_playback_dma_ulaw,
  snd_pcm_dma_move,
  snd_pcm_playback_dma_neutral
};

static struct snd_stru_pcm_hardware snd_es1688_record = {
  NULL,				/* private data */
  NULL,				/* private free */
  SND_PCM_HW_AUTODMA,		/* flags */
  SND_PCM_FMT_U8 | SND_PCM_FMT_S16_LE,	/* formats */
  0,				/* align value */
  6,				/* minimal fragment */
  4000,				/* min. rate */
  48000,			/* max. rate */
  2,				/* max. voices */
  snd_es1688_record_open,
  snd_es1688_record_close,
  snd_es1688_record_compute_rate,
  snd_es1688_record_prepare,
  snd_es1688_record_trigger,
  snd_es1688_record_pointer,
  snd_pcm_record_dma_ulaw,
  snd_pcm_dma_move,
  NULL
};

static void snd_es1688_free( void *private_data )
{
  snd_free( private_data, sizeof( es1688_t ) );
}

snd_pcm_t *snd_es1688_new_device( snd_card_t *card,
                                  unsigned short port,
                                  unsigned short mpu_port,
				  unsigned short irqnum,
				  unsigned short mpu_irqnum,
			       	  unsigned short dma8num,
				  unsigned short hardware )
{
  snd_pcm_t *pcm;
  es1688_t *codec;

  pcm = snd_pcm_new_device( card, "ESx688" );
  if ( !pcm ) return NULL;
  codec = (es1688_t *)snd_malloc( sizeof( es1688_t ) );
  if ( !codec ) return NULL;
  memset( codec, 0, sizeof( es1688_t ) );
  snd_spin_prepare( codec, reg );
  snd_spin_prepare( codec, mixer );
  codec -> pcm = pcm;
  codec -> card = pcm -> card;
  codec -> port = port;
  codec -> mpu_port = mpu_port;
  codec -> irqnum = irqnum;
  codec -> irq = pcm -> card -> irqs[ irqnum ] -> irq;
  codec -> mpu_irqnum = mpu_irqnum;
  if ( mpu_irqnum != SND_IRQ_DISABLE )
    codec -> mpu_irq = pcm -> card -> irqs[ mpu_irqnum ] -> irq;
  codec -> dma8num = dma8num;
  codec -> dma8 = pcm -> card -> dmas[ dma8num ] -> dma;
  codec -> hardware = hardware;
  memcpy( &pcm -> playback.hw, &snd_es1688_playback, sizeof( snd_es1688_playback ) );
  memcpy( &pcm -> record.hw, &snd_es1688_record, sizeof( snd_es1688_record ) );
  pcm -> private_data = codec;
  pcm -> private_free = snd_es1688_free;
  pcm -> info_flags = SND_PCM_INFO_CODEC | SND_PCM_INFO_MMAP |
                      SND_PCM_INFO_PLAYBACK | SND_PCM_INFO_RECORD;
  sprintf( pcm -> name, "ES%s688 rev %i", codec -> hardware == ES1688_HW_688 ? "" : "1", codec -> version & 0x0f );
  if ( snd_es1688_probe( pcm ) < 0 ) {
    snd_pcm_free( pcm );
    return NULL;
  }
  return pcm;
}

/*
 *  MIXER part
 */

/*
 *    private_value
 *		0x000000ff - register
 *              0x00000f00 - left shift
 *              0x0000f000 - right shift
 */

static void snd_es1688_record_source( snd_kmixer_t *mixer, snd_kmixer_channel_t *channel, int enable )
{  
  unsigned long flags;
  unsigned char mixs = ES1688_MIXS_NONE, mask;
  es1688_t *codec;

  codec = (es1688_t *)mixer -> private_data;
  
  switch ( channel -> hw.priority ) {
    case SND_MIXER_PRI_MIC:	mask = 1; break;
    case SND_MIXER_PRI_CD:	mask = 2; break;
    case SND_MIXER_PRI_LINE:	mask = 4; break;
    default: 			mask = 0; break;	/* master */
  }
  if ( enable )
    mixer -> private_value |= mask;
   else
    mixer -> private_value &= ~mask;

  if ( mixer -> private_value == 0 ) mixs = ES1688_MIXS_NONE; else
  if ( mixer -> private_value & 1 ) mixs = ES1688_MIXS_MIC; else
  if ( mixer -> private_value & 2 ) mixs = ES1688_MIXS_CD; else
  mixs = ES1688_MIXS_LINE;
        
  snd_spin_lock( codec, mixer, &flags );
  mixs |= snd_es1688_mixer_read( codec, ES1688_RECORD_SOURCE ) & ~6;
  snd_es1688_mixer_write( codec, ES1688_RECORD_SOURCE, mixs );
  snd_spin_unlock( codec, mixer, &flags );
}

static void snd_es1688_volume_level( snd_kmixer_t *mixer, snd_kmixer_channel_t *channel, int left, int right )
{
  unsigned long flags;
  unsigned char reg, left_shift, right_shift;
  es1688_t *codec;

  reg = (unsigned char)channel -> hw.private_value;
  left_shift = (unsigned char)((channel -> hw.private_value >> 8) & 0x0f);
  right_shift = (unsigned char)((channel -> hw.private_value >> 12) & 0x0f);
#if 0
  snd_printk( "volume: reg = 0x%x, left_shift = 0x%x, right_shift = 0x%x\n", reg, left_shift, right_shift );
#endif
  left <<= left_shift;
  if ( left_shift != right_shift )
    left |= right << right_shift;
  codec = (es1688_t *)mixer -> private_data;
  snd_spin_lock( codec, mixer, &flags );
  snd_es1688_mixer_write( codec, reg, left );
  snd_spin_unlock( codec, mixer, &flags );
}

#define ES1688_MIXS (sizeof(snd_es1688_mixs)/sizeof(struct snd_stru_mixer_channel_hw))
#define ES1688_PRIVATE( reg, left_shift, right_shift ) (reg|(left_shift << 8)|(right_shift<<8))

static struct snd_stru_mixer_channel_hw snd_es1688_mixs[] = {
  {
    SND_MIXER_PRI_PCM,		/* priority */
    SND_MIXER_PRI_MASTER,	/* parent priority */
    SND_MIXER_ID_PCM,		/* device name */
    SND_MIXER_OSS_PCM,		/* OSS device # */
    0, 1, 0, 0, 0,		/* mute/stereo/record/digital/input */
    0, 15,			/* min, max value */
    -4600, 0, 288,		/* min, max, step - dB */
    ES1688_PRIVATE( ES1688_PCM_DEV, 0, 4 ),
    NULL,			/* compute dB -> linear */
    NULL,			/* compute linear -> dB */
    NULL,			/* record source */
    NULL,			/* set mute */
    snd_es1688_volume_level,	/* set volume level */
  },
  {
    SND_MIXER_PRI_MIC,		/* priority */
    SND_MIXER_PRI_MASTER,	/* parent priority */
    SND_MIXER_ID_MIC,		/* device name */
    SND_MIXER_OSS_MIC,		/* OSS device # */
    0, 1, 1, 1, 1,		/* mute/stereo/record/digital/input */
    0, 15,			/* min, max value */
    -4600, 0, 288,		/* min, max, step - dB */
    ES1688_PRIVATE( ES1688_MIC_DEV, 0, 4 ),
    NULL,			/* compute dB -> linear */
    NULL,			/* compute linear -> dB */
    snd_es1688_record_source,	/* record source */
    NULL,			/* set mute */
    snd_es1688_volume_level,	/* set volume level */
  },
  {
    SND_MIXER_PRI_MASTER,	/* priority */
    SND_MIXER_PRI_PARENT,	/* parent priority */
    SND_MIXER_ID_MASTER,	/* device name */
    SND_MIXER_OSS_VOLUME,	/* OSS device # */
    0, 1, 0, 0,	0,		/* mute/stereo/record/digital/input */
    0, 15,			/* min, max value */
    -4600, 0, 288,		/* min, max, step - dB */
    ES1688_PRIVATE( ES1688_MASTER_DEV, 0, 4 ),
    NULL,			/* compute dB -> linear */
    NULL,			/* compute linear -> dB */
    NULL,			/* record source */
    NULL,			/* set mute */
    snd_es1688_volume_level,	/* set volume level */
  },
  {
    SND_MIXER_PRI_FM,		/* priority */
    SND_MIXER_PRI_MASTER,	/* parent priority */
    SND_MIXER_ID_FM,		/* device name */
    SND_MIXER_OSS_LINE1,	/* OSS device # */
    0, 1, 0, 0,	0,		/* mute/stereo/record/digital/input */
    0, 15,			/* min, max value */
    -4600, 0, 288,		/* min, max, step - dB */
    ES1688_PRIVATE( ES1688_FM_DEV, 0, 4 ),
    NULL,			/* compute dB -> linear */
    NULL,			/* compute linear -> dB */
    NULL,			/* record source */
    NULL,			/* set mute */
    snd_es1688_volume_level,	/* set volume level */
  },
  {
    SND_MIXER_PRI_CD,		/* priority */
    SND_MIXER_PRI_MASTER,	/* parent priority */
    SND_MIXER_ID_CD,		/* device name */
    SND_MIXER_OSS_CD,		/* OSS device # */
    0, 1, 1, 0,	1,		/* mute/stereo/record/digital/input */
    0, 15,			/* min, max value */
    -4600, 0, 288,		/* min, max, step - dB */
    ES1688_PRIVATE( ES1688_CD_DEV, 0, 4 ),
    NULL,			/* compute dB -> linear */
    NULL,			/* compute linear -> dB */
    snd_es1688_record_source,	/* record source */
    NULL,			/* set mute */
    snd_es1688_volume_level,	/* set volume level */
  },
  {
    SND_MIXER_PRI_AUXA,		/* priority */
    SND_MIXER_PRI_MASTER,	/* parent priority */
    SND_MIXER_ID_AUXA,		/* device name */
    SND_MIXER_OSS_LINE2,	/* OSS device # */
    0, 1, 0, 0, 0,		/* mute/stereo/record/digital/input */
    0, 15,			/* min, max value */
    -4600, 0, 288,		/* min, max, step - dB */
    ES1688_PRIVATE( ES1688_AUX_DEV, 0, 4 ),
    NULL,			/* compute dB -> linear */
    NULL,			/* compute linear -> dB */
    NULL,			/* record source */
    NULL,			/* set mute */
    snd_es1688_volume_level,	/* set volume level */
  },
  {
    SND_MIXER_PRI_SPEAKER,	/* priority */
    SND_MIXER_PRI_MASTER,	/* parent priority */
    SND_MIXER_ID_SPEAKER,	/* device name */
    SND_MIXER_OSS_SPEAKER,	/* OSS device # */
    0, 1, 0, 0,	0,		/* mute/stereo/record/digital/input */
    0, 7,			/* max value */
    -4600, 0, 575,		/* min, max, step - dB */
    ES1688_PRIVATE( ES1688_SPEAKER_DEV, 0, 0 ),
    NULL,			/* compute dB -> linear */
    NULL,			/* compute linear -> dB */
    NULL,			/* record source */
    NULL,			/* set mute */
    snd_es1688_volume_level,	/* set volume level */
  },
  {
    SND_MIXER_PRI_LINE,		/* priority */
    SND_MIXER_PRI_MASTER,	/* parent priority */
    SND_MIXER_ID_LINE,		/* device name */
    SND_MIXER_OSS_LINE,		/* OSS device # */
    0, 1, 1, 0, 0,		/* mute/stereo/record/digital/input */
    0, 15,			/* min, max value */
    -4600, 0, 288,		/* min, max, step - dB */
    ES1688_PRIVATE( ES1688_LINE_DEV, 0, 0 ),
    NULL,			/* compute dB -> linear */
    NULL,			/* compute linear -> dB */
    snd_es1688_record_source,	/* record source */
    NULL,			/* set mute */
    snd_es1688_volume_level,	/* set volume level */
  },
};

snd_kmixer_t *snd_es1688_new_mixer( snd_pcm_t *pcm )
{
  int idx;
  es1688_t *codec;
  snd_kmixer_t *mixer;
  snd_kmixer_channel_t *channel;
  
  if ( !pcm || !pcm -> card ) return NULL;
  codec = (es1688_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 < ES1688_MIXS; idx++ ) {
    channel = snd_mixer_new_channel( mixer, &snd_es1688_mixs[ idx ] );
    if ( !channel ) {
      snd_mixer_free( mixer );
      return NULL;
    }
  }
  mixer -> hw.caps = SND_MIXER_INFO_CAP_EXCL_RECORD;
  mixer -> private_data = codec;
  return mixer;
}

/*
 *  INIT part
 */

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

void cleanup_module( void )
{
}
