/*
 *  Copyright (c) by Jaroslav Kysela <perex@jcu.cz>
 *  Routines for control of 8-bit SoundBlaster cards and clones
 *  Code needs to be debugged... I don't have old SB soundcards.
 *
 *
 *   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.
 *
 */

#include "driver.h"
#include "pcm.h"
#include "mixer.h"
#include "sb.h"

static inline void snd_sb8_ack_8bit( sbdsp_t *codec )
{
  inb( SBP( codec, DATA_AVAIL ) );
}

static void snd_sb8_compute_rate( snd_pcm_t *pcm, int direction )
{
  sbdsp_t *codec;
  snd_pcm_channel_t *pchn;
  int rate;
  
  pchn = direction == SND_PCM_PLAYBACK ? &pcm -> playback : &pcm -> record;
  codec = (sbdsp_t *)pcm -> private_data;
  pchn -> real_rate = rate = pchn -> rate;
  switch ( codec -> hardware ) {
    case SB_HW_10:
    case SB_HW_20:
      if ( direction == SND_PCM_RECORD && rate > 13000 ) rate = 13000;
      if ( rate < 4000 ) pchn -> real_rate = 4000;
      if ( rate > 23000 ) rate = 23000;
      codec -> speed8 = (65535 - (256000000 + rate / 2) / rate) >> 8;
      if ( direction == SND_PCM_PLAYBACK ) {
        codec -> fmt8 = SB_DSP_LO_OUTPUT_AUTO;	/* not used with SB 1.0 */
      } else {
        codec -> fmt8 = SB_DSP_LO_INPUT_AUTO;	/* not used with SB 1.0 */
      }
      pchn -> real_rate = 256000000 / (65536 - (codec -> speed8 << 8));
      break;
    case SB_HW_201:
      if ( direction == SND_PCM_RECORD && rate > 15000 ) rate = 15000;
      if ( rate < 4000 ) rate = 4000;
      if ( rate > 44100 ) rate = 44100;
      codec -> speed8 = (65536 - (256000000 + rate / 2) / rate) >> 8;
      if ( direction == SND_PCM_PLAYBACK  ) {
        codec -> fmt8 = rate > 23000 ? SB_DSP_HI_OUTPUT_AUTO : SB_DSP_LO_OUTPUT_AUTO;
      } else {
        codec -> fmt8 = rate > 13000 ? SB_DSP_HI_INPUT_AUTO : SB_DSP_LO_INPUT_AUTO;
      }
      pchn -> real_rate = 256000000 / (65536 - (codec -> speed8 << 8));
      break;
    case SB_HW_PRO:
      if ( rate < 4000 ) rate = 4000;
      if ( direction == SND_PCM_RECORD ) {
        if ( rate > 15000 ) rate = 15000;
      }
      if ( pchn -> voices > 1 ) {
        rate = rate > (22050 + 11025) / 2 ? 22050 : 11025;
        codec -> speed8 = (65536 - (256000000 + rate) / (rate << 1)) >> 8;
        if ( direction == SND_PCM_PLAYBACK ) {
          codec -> fmt8 = SB_DSP_HI_OUTPUT_AUTO;
        } else {
          codec -> fmt8 = SB_DSP_HI_INPUT_AUTO;
        }
      } else {
        if ( rate > 44100 ) rate = 44100;
        codec -> speed8 = (65536 - (256000000 + rate / 2) / rate) >> 8;
        if ( direction == SND_PCM_PLAYBACK ) {
          codec -> fmt8 = rate > 23000 ? SB_DSP_HI_OUTPUT_AUTO : SB_DSP_LO_OUTPUT_AUTO;
        } else {
          codec -> fmt8 = rate > 23000 ? SB_DSP_HI_INPUT_AUTO : SB_DSP_LO_INPUT_AUTO;
        }
      }
      pchn -> real_rate = (256000000 / (65536 - (codec -> speed8 << 8))) / pchn -> voices;
#if 0
      snd_printd( "snd_sb8_compute_rate: rate = %u, pchn->real_rate = %u\n", rate, pchn->real_rate );
#endif
      break;
    default:
      snd_printd( "unknown hardware for sb8 compute rate!!!\n" );
  }
}

static void snd_sb8_playback_compute_rate( snd_pcm_t *pcm )
{
  snd_sb8_compute_rate( pcm, SND_PCM_PLAYBACK );  
}

static void snd_sb8_record_compute_rate( snd_pcm_t *pcm )
{
  snd_sb8_compute_rate( pcm, SND_PCM_RECORD );
}

static void snd_sb8_playback_prepare( snd_pcm_t *pcm,
                                      unsigned char *buffer,
                                      unsigned int size,
                                      unsigned int offset,
                                      unsigned int count )
{
  unsigned long flags, flags1;
  sbdsp_t *codec;
  snd_pcm_channel_t *pchn;
  
  pchn = &pcm -> playback;
  codec = (sbdsp_t *)pcm -> private_data;
  switch ( codec -> hardware ) {
    case SB_HW_10:
      snd_spin_lock( codec, reg, &flags );
      snd_sb8_ack_8bit( codec );
      snd_sbdsp_command( codec, SB_DSP_SAMPLE_RATE );
      snd_sbdsp_command( codec, codec -> speed8 );
      snd_sb8_ack_8bit( codec );
      snd_sbdsp_command( codec, SB_DSP_SPEAKER_ON );
      snd_spin_unlock( codec, reg, &flags );
      snd_dma_program( codec -> dma8, &buffer[ offset ], count, DMA_MODE_WRITE );
      codec -> count8 = count - 1;
      break;
    case SB_HW_20:
    case SB_HW_201:
    case SB_HW_PRO:
      snd_spin_lock( codec, reg, &flags );
      snd_sbdsp_command( codec, SB_DSP_SPEAKER_OFF );
      if ( snd_sbdsp_reset( codec ) < 0 ) {
        snd_spin_unlock( codec, reg, &flags );
        snd_printk( "sb8 - reset failed!!!\n" );
        return;
      }
      snd_sbdsp_command( codec, SB_DSP_SPEAKER_OFF );
      if ( codec -> hardware == SB_HW_PRO ) {
        snd_spin_lock( &codec -> mixer, mixer, &flags1 );
        if ( pchn -> voices > 1 ) {
          snd_sbmixer_write( &codec -> mixer, 0x0e, snd_sbmixer_read( &codec -> mixer, 0x0e ) | 0x02 );
        } else {
          snd_sbmixer_write( &codec -> mixer, 0x0e, snd_sbmixer_read( &codec -> mixer, 0x0e ) & ~0x02 );
        }
        snd_spin_unlock( &codec -> mixer, mixer, &flags1 );
      }
      snd_sb8_ack_8bit( codec );
      snd_sbdsp_command( codec, SB_DSP_SAMPLE_RATE );
      snd_sbdsp_command( codec, codec -> speed8 );
      snd_sb8_ack_8bit( codec );
      snd_spin_unlock( codec, reg, &flags );
      snd_dma_program( codec -> dma8, buffer, size, DMA_MODE_WRITE | DMA_MODE_AUTOINIT );
      snd_sbdsp_command( codec, SB_DSP_SPEAKER_ON );
      count--;
      snd_spin_lock( codec, reg, &flags );
      snd_sbdsp_command( codec, SB_DSP_BLOCK_SIZE );
      snd_sbdsp_command( codec, count & 0xff );
      snd_sbdsp_command( codec, count >> 8 );
      snd_sbdsp_command( codec, codec -> fmt8 );
      snd_sbdsp_command( codec, SB_DSP_DMA8_OFF );
      snd_spin_unlock( codec, reg, &flags );
      break;
    default:
      snd_printd( "unknown hardware for sb8 playback prepare!!!\n" );      
  }
}

static void snd_sb8_playback_trigger( snd_pcm_t *pcm, int up )
{
  unsigned long flags;
  sbdsp_t *codec;

  codec = (sbdsp_t *)pcm -> private_data;
  snd_spin_lock( codec, reg, &flags );
  switch ( codec -> hardware ) {
    case SB_HW_10:
      if ( up ) {
        codec -> mode8 = SB_MODE8_PLAYBACK;
        if ( snd_sbdsp_command( codec, SB_DSP_OUTPUT ) ) {
          snd_sbdsp_command( codec, codec -> count8 & 0xff );
          snd_sbdsp_command( codec, codec -> count8 >> 8 );
        } else {
          snd_printk( "SB 1.0 - unable to start output\n" );
        }
      } else {
        snd_sbdsp_reset( codec );
        codec -> mode8 = SB_MODE8_HALT;
      }
      break;
    case SB_HW_20:
    case SB_HW_201:
    case SB_HW_PRO:
      snd_sbdsp_command( codec, up ? SB_DSP_DMA8_ON : SB_DSP_DMA8_OFF );
      codec -> mode8 = up ? SB_MODE8_PLAYBACK : SB_MODE8_HALT;
      break;
  }
  snd_spin_unlock( codec, reg, &flags );
}

static void snd_sb8_record_prepare( snd_pcm_t *pcm,
                                    unsigned char *buffer,
                                    unsigned int size,
                                    unsigned int offset,
                                    unsigned int count )
{
  unsigned long flags, flags1;
  sbdsp_t *codec;
  snd_pcm_channel_t *pchn;
  
  pchn = &pcm -> record;
  codec = (sbdsp_t *)pcm -> private_data;
  switch ( codec -> hardware ) {
    case SB_HW_10:
      snd_spin_lock( codec, reg, &flags );
      snd_sbdsp_command( codec, SB_DSP_SPEAKER_OFF );
      snd_sb8_ack_8bit( codec );
      snd_sbdsp_command( codec, SB_DSP_SAMPLE_RATE );
      snd_sbdsp_command( codec, codec -> speed8 );
      snd_sb8_ack_8bit( codec );
      snd_spin_unlock( codec, reg, &flags );
      snd_dma_program( codec -> dma8, &buffer[ offset ], count, DMA_MODE_READ );
      codec -> count8 = count - 1;
      break;
    case SB_HW_20:
    case SB_HW_201:
    case SB_HW_PRO:
      snd_spin_lock( codec, reg, &flags );
      snd_sbdsp_command( codec, SB_DSP_SPEAKER_OFF );
      if ( snd_sbdsp_reset( codec ) < 0 ) {
        snd_spin_unlock( codec, reg, &flags );
        snd_printk( "sb8 - reset failed!!!\n" );
        return;
      }
      snd_sbdsp_command( codec, SB_DSP_SPEAKER_OFF );
      if ( codec -> hardware == SB_HW_PRO ) {
        snd_spin_lock( &codec -> mixer, mixer, &flags1 );
        if ( pchn -> voices > 1 ) {
          snd_sbmixer_write( &codec -> mixer, 0x0e, snd_sbmixer_read( &codec -> mixer, 0x0e ) | 0x02 );
        } else {
          snd_sbmixer_write( &codec -> mixer, 0x0e, snd_sbmixer_read( &codec -> mixer, 0x0e ) & ~0x02 );
        }
        snd_spin_unlock( &codec -> mixer, mixer, &flags1 );
      }
      if ( codec -> hardware == SB_HW_PRO ) {
        snd_sbdsp_command( codec, pcm -> record.voices > 1 ? SB_DSP_STEREO_8BIT : SB_DSP_MONO_8BIT );
      }
      snd_sb8_ack_8bit( codec );
      snd_sbdsp_command( codec, SB_DSP_SAMPLE_RATE );
      snd_sbdsp_command( codec, codec -> speed8 );
      snd_sb8_ack_8bit( codec );
      snd_spin_unlock( codec, reg, &flags );
      snd_dma_program( codec -> dma8, buffer, size, DMA_MODE_READ | DMA_MODE_AUTOINIT );
      count--;
      snd_spin_lock( codec, reg, &flags );
      snd_sbdsp_command( codec, SB_DSP_BLOCK_SIZE );
      snd_sbdsp_command( codec, count & 0xff );
      snd_sbdsp_command( codec, count >> 8 );
      snd_sbdsp_command( codec, codec -> fmt8 );
      snd_sbdsp_command( codec, SB_DSP_DMA8_OFF );
      snd_spin_unlock( codec, reg, &flags );
      break;
    default:
      snd_printd( "unknown hardware for sb8 record prepare!!!\n" );      
  }
}

static void snd_sb8_record_trigger( snd_pcm_t *pcm, int up )
{
  unsigned long flags;
  sbdsp_t *codec;

  codec = (sbdsp_t *)pcm -> private_data;
  snd_spin_lock( codec, reg, &flags );
  switch ( codec -> hardware ) {
    case SB_HW_10:
      if ( up ) {
        codec -> mode8 = SB_MODE8_RECORD;
        if ( snd_sbdsp_command( codec, SB_DSP_INPUT ) ) {
          snd_sbdsp_command( codec, codec -> count8 & 0xff );
          snd_sbdsp_command( codec, codec -> count8 >> 8 );
        } else {
          snd_printk( "SB 1.0 - unable to start input\n" );
        }
      } else {
        snd_sbdsp_reset( codec );
        codec -> mode8 = SB_MODE8_HALT;
      }
      break;
    case SB_HW_20:
    case SB_HW_201:
    case SB_HW_PRO:
      snd_sbdsp_command( codec, up ? SB_DSP_DMA8_ON : SB_DSP_DMA8_OFF );
      codec -> mode8 = up ? SB_MODE8_RECORD : SB_MODE8_HALT;
      break;
  }
  snd_spin_unlock( codec, reg, &flags );
}

void snd_sbdsp_sb8_interrupt( snd_pcm_t *pcm )
{
  sbdsp_t *codec;
  
  codec = (sbdsp_t *)pcm -> private_data;
  switch ( codec -> mode8 ) {
    case SB_MODE8_PLAYBACK:		/* ok.. playback is active */
      pcm -> playback.ack( pcm );
      break;
    case SB_MODE8_RECORD:
      pcm -> record.ack( pcm );
      break;
  }
  snd_sb8_ack_8bit( codec );
}

/*
 *
 */

static int snd_sb8_playback_open( snd_pcm_t *pcm )
{
  unsigned long flags;
  sbdsp_t *codec;
  int err;

  codec = (sbdsp_t *)pcm -> private_data;
  snd_spin_lock( codec, open8, &flags );
  if ( codec -> open8 ) {
    snd_spin_unlock( codec, open8, &flags );
    return -EBUSY;
  }
  codec -> open8 |= SB_OPEN_PCM;
  snd_spin_unlock( codec, open8, &flags );
  if ( (err = snd_pcm_dma_alloc( pcm, SND_PCM_PLAYBACK, codec -> dma8num, "Sound Blaster DSP" )) < 0 ) {
    snd_spin_lock( codec, open8, &flags );
    codec -> open8 &= ~SB_OPEN_PCM;
    snd_spin_unlock( codec, open8, &flags );
    return err;
  }
  return 0;
}

static int snd_sb8_record_open( snd_pcm_t *pcm )
{
  unsigned long flags;
  sbdsp_t *codec;
  int err;

  codec = (sbdsp_t *)pcm -> private_data;
  snd_spin_lock( codec, open8, &flags );
  if ( codec -> open8 ) {
    snd_spin_unlock( codec, open8, &flags );
    return -EBUSY;
  }
  codec -> open8 |= SB_OPEN_PCM;
  snd_spin_unlock( codec, open8, &flags );
  if ( (err = snd_pcm_dma_alloc( pcm, SND_PCM_RECORD, codec -> dma8num, "Sound Blaster DSP" )) < 0 ) {
    snd_spin_lock( codec, open8, &flags );
    codec -> open8 &= ~SB_OPEN_PCM;
    snd_spin_unlock( codec, open8, &flags );
    return err;
  }
  return 0;
}

static void snd_sb8_playback_close( snd_pcm_t *pcm )
{
  unsigned long flags;
  sbdsp_t *codec;
  snd_card_t *card;

  card = pcm -> card;
  codec = (sbdsp_t *)pcm -> private_data;
  snd_pcm_dma_free( pcm, SND_PCM_PLAYBACK, codec -> dma8num );
  snd_spin_lock( codec, open8, &flags );
  codec -> open8 &= ~SB_OPEN_PCM;
  snd_spin_unlock( codec, open8, &flags );
}

static void snd_sb8_record_close( snd_pcm_t *pcm )
{
  unsigned long flags;
  sbdsp_t *codec;
  snd_card_t *card;

  card = pcm -> card;
  codec = (sbdsp_t *)pcm -> private_data;
  snd_pcm_dma_free( pcm, SND_PCM_RECORD, codec -> dma8num );
  snd_spin_lock( codec, open8, &flags );
  codec -> open8 &= ~SB_OPEN_PCM;
  snd_spin_unlock( codec, open8, &flags );
}

static unsigned int snd_sb8_playback_pointer( snd_pcm_t *pcm, unsigned int used_size )
{
  sbdsp_t *codec;

  codec = (sbdsp_t *)pcm -> private_data;
  if ( codec -> mode8 != SB_MODE8_PLAYBACK ) return 0;
  return used_size - snd_dma_residue( codec -> dma8 );
}

static unsigned int snd_sb8_record_pointer( snd_pcm_t *pcm, unsigned int used_size )
{
  sbdsp_t *codec;

  codec = (sbdsp_t *)pcm -> private_data;
  if ( codec -> mode8 != SB_MODE8_RECORD ) return 0;
  return used_size - snd_dma_residue( codec -> dma8 );
}

/*
 *
 */

struct snd_stru_pcm_hardware snd_sb8_playback = {
  NULL,				/* private data */
  NULL,				/* private_free */
  SND_PCM_HW_AUTODMA,		/* flags */
  SND_PCM_FMT_U8,		/* formats */
  0,				/* align value */
  6,				/* minimal fragment */
  4000,				/* min. rate */
  23000,			/* max. rate */
  1,				/* max. voices */
  snd_sb8_playback_open,
  snd_sb8_playback_close,
  snd_sb8_playback_compute_rate,
  snd_sb8_playback_prepare,
  snd_sb8_playback_trigger,
  snd_sb8_playback_pointer,
  snd_pcm_playback_dma_ulaw,
  snd_pcm_dma_move,
  snd_pcm_playback_dma_neutral
};

struct snd_stru_pcm_hardware snd_sb8_record = {
  NULL,				/* private data */
  NULL,				/* private free */
  SND_PCM_HW_AUTODMA,		/* flags */
  SND_PCM_FMT_U8,		/* formats */
  0,				/* align value */
  6,				/* minimal fragment */
  4000,				/* min. rate */
  13000,			/* max. rate */
  1,				/* max. voices */
  snd_sb8_record_open,
  snd_sb8_record_close,
  snd_sb8_record_compute_rate,
  snd_sb8_record_prepare,
  snd_sb8_record_trigger,
  snd_sb8_record_pointer,
  snd_pcm_record_dma_ulaw,
  snd_pcm_dma_move,
  NULL
};
