/*
 *  Copyright (c) by Jaroslav Kysela <perex@jcu.cz>
 *  Routines for control of 16-bit SoundBlaster cards and clones
 *  Note: This is very ugly hardware which uses one 8-bit DMA channel and
 *        second 16-bit DMA channel. Unfortunately 8-bit DMA channel can't
 *        transfer 16-bit samples and 16-bit DMA channels can't transfer
 *        8-bit samples. This make full duplex more complicated than
 *        can be... People, don't buy these shity soundcards for full 16-bit
 *        duplex!!!
 *  Note: 16-bit wide is assigned to first direction which made request.
 *        With full duplex - playback is preferred with abstract layer.
 *
 *
 *   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 "info.h"
#include "sb.h"
#include "ulaw.h"

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

static inline void snd_sb16_ack_16bit( sbdsp_t *codec )
{
  inb( SBP( codec, DATA_AVAIL_16 ) );
}

static void snd_sb16_playback_8to16( unsigned char *buffer,
                                     unsigned int offset,
                                     unsigned char *user,
                                     unsigned int count,
                                     int ulaw )
{
  register unsigned char *pbuffer;
  register unsigned int size1;
  unsigned char sbuffer[ 512 ];
  unsigned int size;

#if 0
  snd_printk( "p8to16: buffer = 0x%lx, offset = %i, count = %i, ulaw = %i\n", (long)buffer, offset, count, ulaw );
#endif
  buffer += offset << 1;
  while ( count > 0 ) {
    size = size1 = sizeof( sbuffer ) < count ? sizeof( sbuffer ) : count;
    copy_from_user( sbuffer, user, size );
    pbuffer = sbuffer;
    if ( ulaw ) {
      while ( size1-- > 0 ) {
        *buffer++ = 0x80;
        *buffer++ = snd_ulaw_dsp_loud[ *pbuffer++ ];
      }
    } else {
      while ( size1-- > 0 ) {
        *buffer++ = 0x80;
        *buffer++ = *pbuffer++;
      }
    }
    count -= size;
    user += size;
  }  
}
				     
static void snd_sb16_record_16to8( unsigned char *buffer,
                                   unsigned int offset,
                                   unsigned char *user,
                                   unsigned int count,
                                   int ulaw )
{
  register unsigned char *pbuffer;
  register unsigned int size1;
  unsigned char sbuffer[ 512 ];
  unsigned int size;

#if 0
  snd_printk( "r16to8: buffer = 0x%lx, offset = %i, count = %i, ulaw = %i\n", (long)buffer, offset, count, ulaw );
#endif
  buffer += (offset << 1) + 1;
  while ( count > 0 ) {
    size = size1 = sizeof( sbuffer ) < count ? sizeof( sbuffer ) : count;
    pbuffer = sbuffer;	/* little endian */
    if ( ulaw ) {
      while ( size1-- > 0 ) {
        *pbuffer++ = snd_dsp_ulaw_loud[ *buffer ];
        buffer += 2;
      }
    } else {
      while ( size1-- > 0 ) {
        *pbuffer++ = *buffer;
        buffer += 2;
      }
    }
    copy_to_user( user, sbuffer, size );
    count -= size;
    user += size;
  }  
}
				     
static void snd_sb16_playback_16to8( unsigned char *buffer,
                                     unsigned int offset,
                                     unsigned char *user,
                                     unsigned int count )
{
  register unsigned char *pbuffer;
  register unsigned int size1;
  unsigned char sbuffer[ 512 ];
  unsigned int size;

#if 0
  snd_printk( "p8to16: buffer = 0x%lx, offset = %i, count = %i\n", (long)buffer, offset, count );
#endif
  buffer += offset >> 1;
  while ( count > 0 ) {
    size = size1 = sizeof( sbuffer ) < count ? sizeof( sbuffer ) : count;
    copy_from_user( sbuffer, user, size );
    pbuffer = sbuffer + 1;	/* little endian data */
    size1 >>= 1;
    while ( size1-- > 0 ) {
      *buffer++ = *pbuffer;
      pbuffer += 2;
    }
    count -= size;
    user += size;
  }
}

static void snd_sb16_record_8to16( unsigned char *buffer,
                                   unsigned int offset,
                                   unsigned char *user,
                                   unsigned int count )
{
  register unsigned char *pbuffer;
  register unsigned int size1;
  unsigned char sbuffer[ 512 ];
  unsigned int size;

#if 0
  snd_printk( "p8to16: buffer = 0x%lx, offset = %i, count = %i\n", (long)buffer, offset, count );
#endif
  buffer += offset >> 1;
  while ( count > 0 ) {
    size = size1 = sizeof( sbuffer ) < count ? sizeof( sbuffer ) : count;
    pbuffer = sbuffer;		/* little endian */
    size1 >>= 1;
    while ( size1-- > 0 ) {
      *pbuffer++ = 0x80;
      *pbuffer++ = *buffer++;
    }
    copy_to_user( user, sbuffer, size );
    count -= size;
    user += size;
  }  
}
				     
static void snd_sb16_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_sb16_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_sb16_setup_rate( sbdsp_t *codec, unsigned short rate, int direction )
{
  unsigned long flags;

  snd_spin_lock( codec, reg, &flags );
  if ( codec -> mode16 & (direction == SND_PCM_PLAYBACK ? SB_MODE16_PLAYBACK16 : SB_MODE16_RECORD16) )
    snd_sb16_ack_16bit( codec );
   else
    snd_sb16_ack_8bit( codec );
  if ( !(codec -> mode16 & SB_MODE16_RATE_LOCK) ) {
    snd_sbdsp_command( codec, SB_DSP_SAMPLE_RATE_IN );
    snd_sbdsp_command( codec, rate >> 8 );
    snd_sbdsp_command( codec, rate & 0xff );
    snd_sbdsp_command( codec, SB_DSP_SAMPLE_RATE_OUT );
    snd_sbdsp_command( codec, rate >> 8 );
    snd_sbdsp_command( codec, rate & 0xff );
  }
  snd_spin_unlock( codec, reg, &flags );
}

static void snd_sb16_playback_prepare( snd_pcm_t *pcm,
                                       unsigned char *buffer,
                                       unsigned int size,
                                       unsigned int offset,
                                       unsigned int count )
{
  unsigned long flags;
  sbdsp_t *codec;
  snd_pcm_channel_t *pchn;
  unsigned char format;
  
  pchn = &pcm -> playback;
  codec = (sbdsp_t *)pcm -> private_data;
  format = SB_DSP4_MODE_UNS_MONO;
  if ( pchn -> mode & SND_PCM_MODE_U ) {
    format = pchn -> voices > 1 ? SB_DSP4_MODE_UNS_STEREO : SB_DSP4_MODE_UNS_MONO;
  } else {
    format = pchn -> voices > 1 ? SB_DSP4_MODE_SIGN_STEREO : SB_DSP4_MODE_SIGN_MONO;
  }
  snd_sb16_setup_rate( codec, pchn -> real_rate, SND_PCM_PLAYBACK );
#if 0
  snd_printk( "playback - buffer = 0x%lx, size = %i (%i), count = %i\n", (long)buffer, size, pchn -> size, count );
#endif
  if ( codec -> mode16 & SB_MODE16_PLAYBACK16 ) {	/* use 16-bit DMA */
    if ( !(pchn -> mode & SND_PCM_MODE_16) ) {
      size <<= 1;
      count <<= 1;
    }
    snd_dma_program( codec -> dma16, buffer, size, DMA_MODE_WRITE | DMA_MODE_AUTOINIT );
    count >>= 1; count--;
    snd_spin_lock( codec, reg, &flags );
    snd_sbdsp_command( codec, SB_DSP_SPEAKER_ON );
    snd_sbdsp_command( codec, SB_DSP4_OUT16_AI );
    snd_sbdsp_command( codec, format );
    snd_sbdsp_command( codec, count & 0xff );
    snd_sbdsp_command( codec, count >> 8 );
    snd_sbdsp_command( codec, SB_DSP_DMA16_OFF );
    snd_spin_unlock( codec, reg, &flags );
  } else {
    if ( pchn -> mode & SND_PCM_MODE_16 ) {
      size >>= 1;
      count >>= 1;
    }
    snd_dma_program( codec -> dma8, buffer, size, DMA_MODE_WRITE | DMA_MODE_AUTOINIT );
    count--;
    snd_spin_lock( codec, reg, &flags );
    snd_sbdsp_command( codec, SB_DSP_SPEAKER_ON );
    snd_sbdsp_command( codec, SB_DSP4_OUT8_AI );
    snd_sbdsp_command( codec, format );
    snd_sbdsp_command( codec, count & 0xff );
    snd_sbdsp_command( codec, count >> 8 );
    snd_sbdsp_command( codec, SB_DSP_DMA8_OFF );
    snd_spin_unlock( codec, reg, &flags );
  }
}

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

  codec = (sbdsp_t *)pcm -> private_data;
#if 0
  snd_printk( "playback trigger - %i\n", up );
#endif
  snd_spin_lock( codec, reg, &flags );
  if ( up ) {
    snd_sbdsp_command( codec, SB_DSP_SPEAKER_ON );
    snd_sbdsp_command( codec, codec -> mode16 & SB_MODE16_PLAYBACK16 ? SB_DSP_DMA16_ON : SB_DSP_DMA8_ON );
    codec -> mode16 |= SB_MODE16_RATE_LOCK_P;
  } else {
    codec -> mode16 &= ~SB_MODE16_RATE_LOCK_P;
    snd_sbdsp_command( codec, codec -> mode16 & SB_MODE16_PLAYBACK16 ? SB_DSP_DMA16_OFF : SB_DSP_DMA8_OFF );
    snd_sbdsp_command( codec, SB_DSP_SPEAKER_OFF );
    /* next two lines are needed for some types of DSP4 (SB AWE 32 - 4.13) */
    if ( codec -> mode16 & SB_MODE16_RATE_LOCK_R ) {
#if 0
      snd_printk( "record trigger!!!! (from playback)" );
#endif
      snd_sbdsp_command( codec, codec -> mode16 & SB_MODE16_RECORD16 ? SB_DSP_DMA16_ON : SB_DSP_DMA8_ON );
    }
  }
  snd_spin_unlock( codec, reg, &flags );
}

static void snd_sb16_record_prepare( snd_pcm_t *pcm,
                                     unsigned char *buffer,
                                     unsigned int size,
                                     unsigned int offset,
                                     unsigned int count )
{
  unsigned long flags;
  sbdsp_t *codec;
  snd_pcm_channel_t *pchn;
  unsigned char format;
  
  pchn = &pcm -> record;
  codec = (sbdsp_t *)pcm -> private_data;
  format = SB_DSP4_MODE_UNS_MONO;
  if ( pchn -> mode & SND_PCM_MODE_U ) {
    format = pchn -> voices > 1 ? SB_DSP4_MODE_UNS_STEREO : SB_DSP4_MODE_UNS_MONO;
  } else {
    format = pchn -> voices > 1 ? SB_DSP4_MODE_SIGN_STEREO : SB_DSP4_MODE_SIGN_MONO;
  }
  snd_sb16_setup_rate( codec, pchn -> real_rate, SND_PCM_RECORD );
#if 0
  snd_printk( "record - buffer = 0x%lx, size = %i (%i), count = %i\n", (long)buffer, size, pchn -> size, count );
#endif
  if ( codec -> mode16 & SB_MODE16_RECORD16 ) {		/* use 16-bit DMA */
    if ( !(pchn -> mode & SND_PCM_MODE_16) ) {
      size <<= 1;
      count <<= 1;
    }
    snd_dma_program( codec -> dma16, buffer, size, DMA_MODE_READ | DMA_MODE_AUTOINIT );
    count >>= 1; count--;
    snd_spin_lock( codec, reg, &flags );
    snd_sbdsp_command( codec, SB_DSP4_IN16_AI );
    snd_sbdsp_command( codec, format );
    snd_sbdsp_command( codec, count & 0xff );
    snd_sbdsp_command( codec, count >> 8 );
    snd_sbdsp_command( codec, SB_DSP_DMA16_OFF );
    snd_spin_unlock( codec, reg, &flags );
  } else {
    if ( pchn -> mode & SND_PCM_MODE_16 ) {
      size >>= 1;
      count >>= 1;
    }
    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_DSP4_IN8_AI );
    snd_sbdsp_command( codec, format );
    snd_sbdsp_command( codec, count & 0xff );
    snd_sbdsp_command( codec, count >> 8 );
    snd_sbdsp_command( codec, SB_DSP_DMA8_OFF );
    snd_spin_unlock( codec, reg, &flags );
  }  
}

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

#if 0
  snd_printk( "sb16: record trigger - %i\n", up );
#endif
  codec = (sbdsp_t *)pcm -> private_data;
  snd_spin_lock( codec, reg, &flags );
  if ( up ) {
    codec -> mode16 |= SB_MODE16_RATE_LOCK_R;
    snd_sbdsp_command( codec, codec -> mode16 & SB_MODE16_RECORD16 ? SB_DSP_DMA16_ON : SB_DSP_DMA8_ON );
  } else {
    codec -> mode16 &= ~SB_MODE16_RATE_LOCK_R;
    snd_sbdsp_command( codec, codec -> mode16 & SB_MODE16_RECORD16 ? SB_DSP_DMA16_OFF : SB_DSP_DMA8_OFF );
    /* next two lines are needed for some types of DSP4 (SB AWE 32 - 4.13) */
    if ( codec -> mode16 & SB_MODE16_RATE_LOCK_P )
      snd_sbdsp_command( codec, codec -> mode16 & SB_MODE16_PLAYBACK16 ? SB_DSP_DMA16_ON : SB_DSP_DMA8_ON );
  }
  snd_spin_unlock( codec, reg, &flags );
}

void snd_sbdsp_sb16_interrupt( snd_pcm_t *pcm, unsigned short status )
{
  int ok;
  sbdsp_t *codec;
  unsigned long flags;
  
  codec = (sbdsp_t *)pcm -> private_data;
#if 0
  PRINTK( "sb16: interrupt - status = 0x%x\n", status );
#endif
  if ( status & 0x01 ) {
    ok = 0;
    if ( (codec -> mode16 & (SB_MODE16_PLAYBACK16|SB_MODE16_PLAYBACK)) == SB_MODE16_PLAYBACK ) {
      pcm -> playback.ack( pcm ); ok++;
    }
    if ( (codec -> mode16 & (SB_MODE16_RECORD16|SB_MODE16_RECORD)) == SB_MODE16_RECORD ) {
      pcm -> record.ack( pcm ); ok++;
    }
    snd_spin_lock( codec, reg, &flags );
    if ( !ok )
      snd_sbdsp_command( codec, SB_DSP_DMA8_OFF );
    snd_sb16_ack_8bit( codec );
    snd_spin_unlock( codec, reg, &flags );
  }
  if ( status & 0x02 ) {
    ok = 0;
    if ( (codec -> mode16 & (SB_MODE16_PLAYBACK16|SB_MODE16_PLAYBACK)) == (SB_MODE16_PLAYBACK16|SB_MODE16_PLAYBACK) ) {
      pcm -> playback.ack( pcm ); ok++;
    }
    if ( (codec -> mode16 & (SB_MODE16_RECORD16|SB_MODE16_RECORD)) == (SB_MODE16_RECORD16|SB_MODE16_RECORD) ) {
      pcm -> record.ack( pcm ); ok++;
    }
    snd_spin_lock( codec, reg, &flags );
    if ( !ok )
      snd_sbdsp_command( codec, SB_DSP_DMA16_OFF );
    snd_sb16_ack_16bit( codec );
    snd_spin_unlock( codec, reg, &flags );
  }
}

/*
 *
 */

static int snd_sb16_playback_open( snd_pcm_t *pcm )
{
  unsigned long flags;
  sbdsp_t *codec;
  int err = -EBUSY;

  codec = (sbdsp_t *)pcm -> private_data;
  if ( codec -> dma16num != SND_DMA_DISABLE && (codec -> force_mode16 & (SB_MODE16_PLAYBACK|SB_MODE16_AUTO)) ) {
    if ( (err = snd_pcm_dma_alloc( pcm, SND_PCM_PLAYBACK, codec -> dma16num, "Sound Blaster 16 DSP" )) >= 0 ) {
      snd_spin_lock( codec, open16, &flags );
      codec -> mode16 |= SB_MODE16_PLAYBACK16|SB_MODE16_PLAYBACK;
      pcm -> playback.hw.flags &= ~(SND_PCM_HW_8BITONLY|SND_PCM_HW_16BITONLY);
      pcm -> playback.hw.flags |= SND_PCM_HW_16BITONLY;
      snd_spin_unlock( codec, open16, &flags );
      return 0;
    }
  }
  if ( codec -> dma8num != SND_DMA_DISABLE &&
       ( (codec -> force_mode16 & (SB_MODE16_PLAYBACK|SB_MODE16_AUTO)) == SB_MODE16_AUTO ||
         (codec -> force_mode16 & (SB_MODE16_PLAYBACK|SB_MODE16_AUTO)) == 0 )
     ) {
    if ( (err = snd_pcm_dma_alloc( pcm, SND_PCM_PLAYBACK, codec -> dma8num, "Sound Blaster 16 DSP" )) >= 0 ) {
      snd_spin_lock( codec, open16, &flags );
      codec -> mode16 &= ~SB_MODE16_PLAYBACK16;
      codec -> mode16 |= SB_MODE16_PLAYBACK;
      pcm -> playback.hw.flags &= ~(SND_PCM_HW_8BITONLY|SND_PCM_HW_16BITONLY);
      pcm -> playback.hw.flags |= SND_PCM_HW_8BITONLY;
      snd_spin_unlock( codec, open16, &flags );
      return 0;
    }
  }
  return err;
}

static int snd_sb16_record_open( snd_pcm_t *pcm )
{
  unsigned long flags;
  sbdsp_t *codec;
  int err = -EBUSY;

  codec = (sbdsp_t *)pcm -> private_data;
  if ( codec -> dma16num != SND_DMA_DISABLE && (codec -> force_mode16 & (SB_MODE16_RECORD|SB_MODE16_AUTO)) ) {
    if ( (err = snd_pcm_dma_alloc( pcm, SND_PCM_RECORD, codec -> dma16num, "Sound Blaster 16 DSP" )) >= 0 ) {
      snd_spin_lock( codec, open16, &flags );
      codec -> mode16 |= SB_MODE16_RECORD16|SB_MODE16_RECORD;
      pcm -> record.hw.flags &= ~(SND_PCM_HW_8BITONLY|SND_PCM_HW_16BITONLY);
      pcm -> record.hw.flags |= SND_PCM_HW_16BITONLY;
      snd_spin_unlock( codec, open16, &flags );
      return 0;
    }
  }
  if ( codec -> dma8num != SND_DMA_DISABLE &&
       ( (codec -> force_mode16 & (SB_MODE16_RECORD|SB_MODE16_AUTO)) == SB_MODE16_AUTO ||
         (codec -> force_mode16 & (SB_MODE16_RECORD|SB_MODE16_AUTO)) == 0 )
     ) {
    if ( (err = snd_pcm_dma_alloc( pcm, SND_PCM_RECORD, codec -> dma8num, "Sound Blaster 16 DSP" )) >= 0 ) {
      snd_spin_lock( codec, open16, &flags );
      codec -> mode16 &= ~SB_MODE16_RECORD16;
      codec -> mode16 |= SB_MODE16_RECORD;
      pcm -> record.hw.flags &= ~(SND_PCM_HW_8BITONLY|SND_PCM_HW_16BITONLY);
      pcm -> record.hw.flags |= SND_PCM_HW_8BITONLY;
      snd_spin_unlock( codec, open16, &flags );
      return 0;
    }
  }
  return err;
}

static void snd_sb16_playback_close( snd_pcm_t *pcm )
{
  sbdsp_t *codec;

  codec = (sbdsp_t *)pcm -> private_data;
  snd_pcm_dma_free( pcm, SND_PCM_PLAYBACK,
                    codec -> mode16 & SB_MODE16_PLAYBACK16 ?
                    		codec -> dma16num : codec -> dma8num );
  codec -> mode16 &= ~(SB_MODE16_PLAYBACK|SB_MODE16_PLAYBACK16);
  pcm -> playback.hw.flags &= ~(SND_PCM_HW_8BITONLY|SND_PCM_HW_16BITONLY);
}

static void snd_sb16_record_close( snd_pcm_t *pcm )
{
  sbdsp_t *codec;

  codec = (sbdsp_t *)pcm -> private_data;
  snd_pcm_dma_free( pcm, SND_PCM_RECORD,
                    codec -> mode16 & SB_MODE16_RECORD16 ?
                    		codec -> dma16num : codec -> dma8num );
  codec -> mode16 &= ~(SB_MODE16_RECORD|SB_MODE16_RECORD16);
  pcm -> record.hw.flags &= ~(SND_PCM_HW_8BITONLY|SND_PCM_HW_16BITONLY);
}

static unsigned int snd_sb16_playback_pointer( snd_pcm_t *pcm, unsigned int used_size )
{
  sbdsp_t *codec;
  snd_pcm_channel_t *pchn;
  unsigned int result;

  codec = (sbdsp_t *)pcm -> private_data;
  pchn = &pcm -> playback;
  if ( codec -> mode16 & SB_MODE16_PLAYBACK16 ) {
    result = used_size - snd_dma_residue( codec -> dma16 );
    if ( !(pchn -> mode & SND_PCM_MODE_16) ) result >>= 1;
  } else {
    result = used_size - snd_dma_residue( codec -> dma8 );
    if ( pchn -> mode & SND_PCM_MODE_16 ) result <<= 1;
  }
  return result;
}

static unsigned int snd_sb16_record_pointer( snd_pcm_t *pcm, unsigned int used_size )
{
  sbdsp_t *codec;
  snd_pcm_channel_t *pchn;
  unsigned int result;

  codec = (sbdsp_t *)pcm -> private_data;
  pchn = &pcm -> record;
  if ( codec -> mode16 & SB_MODE16_RECORD16 ) {
    result = used_size - snd_dma_residue( codec -> dma16 );
    if ( !(pchn -> mode & SND_PCM_MODE_16) ) result >>= 1;
  } else {
    result = used_size - snd_dma_residue( codec -> dma8 );
    if ( pchn -> mode & SND_PCM_MODE_16 ) result <<= 1;
  }
  return result;
}

static void snd_sb16_playback_dma( snd_pcm_t *pcm,
                                   unsigned char *buffer, unsigned int offset,
                                   unsigned char *user, unsigned int count )
{
  snd_pcm_channel_t *pchn;

  pchn = &pcm -> playback;
  if ( pchn -> hw.flags & SND_PCM_HW_8BITONLY ) {
    if ( pchn -> mode & SND_PCM_MODE_16 ) {
      snd_sb16_playback_16to8( buffer, offset, user, count );
      return;
    }
  }
  if ( pchn -> hw.flags & SND_PCM_HW_16BITONLY ) {
    if ( !(pchn -> mode & SND_PCM_MODE_16) ) {
      snd_sb16_playback_8to16( buffer, offset, user, count, (pchn -> mode & SND_PCM_MODE_ULAW) ? 1 : 0 );
      return;
    }
  }
  if ( pcm -> playback.mode & SND_PCM_MODE_ULAW )
    snd_translate_from_user( snd_ulaw_dsp_loud, &buffer[ offset ], user, count );
   else
    copy_from_user( &buffer[ offset ], user, count );
}

static void snd_sb16_record_dma( snd_pcm_t *pcm,
                                 unsigned char *buffer, unsigned int offset,
                                 unsigned char *user, unsigned int count )
{
  snd_pcm_channel_t *pchn;

  pchn = &pcm -> record;
  if ( pchn -> hw.flags & SND_PCM_HW_8BITONLY ) {
    if ( pchn -> mode & SND_PCM_MODE_16 ) {
      snd_sb16_record_8to16( buffer, offset, user, count );
      return;
    }
  }
  if ( pchn -> hw.flags & SND_PCM_HW_16BITONLY ) {
    if ( !(pchn -> mode & SND_PCM_MODE_16) ) {
      snd_sb16_record_16to8( buffer, offset, user, count, (pchn -> mode & SND_PCM_MODE_ULAW) ? 1 : 0 );
      return;
    }
  }
  if ( pcm -> record.mode & SND_PCM_MODE_ULAW )
    snd_translate_to_user( snd_dsp_ulaw_loud, user, &buffer[ offset ], count );
   else
    copy_to_user( user, &buffer[ offset ], count );
}

static void snd_sb16_playback_dma_move( snd_pcm_t *pcm,
                                        unsigned char *buffer,
                                        unsigned int dest_offset, unsigned int src_offset,
                                        unsigned int count )
{
  snd_pcm_channel_t *pchn;

  pchn = &pcm -> playback;
  if ( pchn -> hw.flags & SND_PCM_HW_8BITONLY ) {
    if ( pchn -> mode & SND_PCM_MODE_16 ) {
      memcpy( &buffer[ dest_offset >> 1 ], &buffer[ src_offset >> 1 ], count >> 1 );
      return;
    }
  }
  if ( pchn -> hw.flags & SND_PCM_HW_16BITONLY ) {
    if ( !(pchn -> mode & SND_PCM_MODE_16) ) {
      memcpy( &buffer[ dest_offset << 1 ], &buffer[ src_offset << 1 ], count << 1 );
      return;
    }
  }
  snd_pcm_dma_move( pcm, buffer, dest_offset, src_offset, count );
}
                                                        
static void snd_sb16_record_dma_move( snd_pcm_t *pcm,
                                      unsigned char *buffer,
                                      unsigned int dest_offset, unsigned int src_offset,
                                      unsigned int count )
{
  snd_pcm_channel_t *pchn;

#if 0
  snd_printk( "record dma move, buffer = 0x%lx, dest_offset = %i, src_offset = %i, count = %i\n", (long)buffer, dest_offset, src_offset, count );
#endif
  pchn = &pcm -> record;
  if ( pchn -> hw.flags & SND_PCM_HW_8BITONLY ) {
    if ( pchn -> mode & SND_PCM_MODE_16 ) {
      memcpy( &buffer[ dest_offset >> 1 ], &buffer[ src_offset >> 1 ], count >> 1 );
      return;
    }
  }
  if ( pchn -> hw.flags & SND_PCM_HW_16BITONLY ) {
    if ( !(pchn -> mode & SND_PCM_MODE_16) ) {
      memcpy( &buffer[ dest_offset << 1 ], &buffer[ src_offset << 1 ], count << 1 );
      return;
    }
  }
  snd_pcm_dma_move( pcm, buffer, dest_offset, src_offset, count );
}

static void snd_sb16_playback_dma_neutral( snd_pcm_t *pcm,
                                           unsigned char *buffer, unsigned int offset,
                                           unsigned int count,
                                           unsigned char neutral_byte )
{
  snd_pcm_channel_t *pchn;

  pchn = &pcm -> playback;
  if ( pchn -> hw.flags & SND_PCM_HW_8BITONLY ) {
    if ( pchn -> mode & SND_PCM_MODE_16 ) {
      memset( &buffer[ offset >> 1 ], neutral_byte, count >> 1 );
      return;
    }
  }
  if ( pchn -> hw.flags & SND_PCM_HW_16BITONLY ) {
    if ( !(pchn -> mode & SND_PCM_MODE_16) ) {
      memset( &buffer[ offset << 1 ], neutral_byte, count << 1 );
      return;
    }
  }
  snd_pcm_playback_dma_neutral( pcm, buffer, offset, count, neutral_byte );
}
                                                                                                           
/*
 *
 */

struct snd_stru_pcm_hardware snd_sb16_playback = {
  NULL,				/* private data */
  NULL,				/* private_free */
  SND_PCM_HW_AUTODMA,		/* flags */
  SND_PCM_FMT_U8 | SND_PCM_FMT_S8 | SND_PCM_FMT_S16_LE | SND_PCM_FMT_U16_LE,	/* formats */
  0,				/* align value */
  6,				/* minimal fragment */
  4000,				/* min. rate */
  44100,			/* max. rate */
  2,				/* max. voices */
  snd_sb16_playback_open,
  snd_sb16_playback_close,
  snd_sb16_playback_compute_rate,
  snd_sb16_playback_prepare,
  snd_sb16_playback_trigger,
  snd_sb16_playback_pointer,
  snd_sb16_playback_dma,
  snd_sb16_playback_dma_move,
  snd_sb16_playback_dma_neutral
};

struct snd_stru_pcm_hardware snd_sb16_record = {
  NULL,				/* private data */
  NULL,				/* private free */
  SND_PCM_HW_AUTODMA,		/* flags */
  SND_PCM_FMT_U8 | SND_PCM_FMT_S8 | SND_PCM_FMT_S16_LE | SND_PCM_FMT_U16_LE,	/* formats */
  0,				/* align value */
  6,				/* minimal fragment */
  4000,				/* min. rate */
  44100,			/* max. rate */
  2,				/* max. voices */
  snd_sb16_record_open,
  snd_sb16_record_close,
  snd_sb16_record_compute_rate,
  snd_sb16_record_prepare,
  snd_sb16_record_trigger,
  snd_sb16_record_pointer,
  snd_sb16_record_dma,
  snd_sb16_record_dma_move,
  NULL
};

/*
 *  /proc interface
 */

static void snd_sb16_proc_read( snd_info_buffer_t *buffer, void *private_data )
{
  sbdsp_t *codec;
  char *tmp, *tmp1;
  
  codec = (sbdsp_t *)private_data;
  snd_iprintf( buffer, "SB16 DMA usage\n\n" );
  if ( codec -> dma8num == SND_DMA_DISABLE || codec -> dma16num == SND_DMA_DISABLE ) {
    snd_iprintf( buffer, "Shared DMA    : %s\n", codec -> dma8num == SND_DMA_DISABLE ? "16-bit" : "8-bit" );
    return;
  }
  tmp = "8-bit";
  if ( codec -> force_mode16 & SB_MODE16_AUTO ) tmp = "auto"; else
  if ( codec -> force_mode16 & SB_MODE16_PLAYBACK ) tmp = "16-bit";
  tmp1 = "inactive";
  if ( codec -> mode16 & SB_MODE16_PLAYBACK ) {
    if ( codec -> mode16 & SB_MODE16_PLAYBACK16 ) tmp1 = "16-bit"; else tmp1 = "8-bit";
  }
  snd_iprintf( buffer, "Playback DMA  : %s (%s)\n", tmp, tmp1 );
  tmp = "8-bit";
  if ( codec -> force_mode16 & SB_MODE16_AUTO ) tmp = "auto"; else
  if ( codec -> force_mode16 & SB_MODE16_RECORD ) tmp = "16-bit";
  tmp1 = "inactive";
  if ( codec -> mode16 & SB_MODE16_RECORD ) {
    if ( codec -> mode16 & SB_MODE16_RECORD16 ) tmp1 = "16-bit"; else tmp1 = "8-bit";
  }
  snd_iprintf( buffer, "Record DMA    : %s (%s)\n", tmp, tmp1 );
}

static void snd_sb16_proc_setup_mode( sbdsp_t *codec, int what )
{
  if ( codec -> dma8num == SND_DMA_DISABLE || codec -> dma16num == SND_DMA_DISABLE )
    return;
  if ( what == 0 ) {
    codec -> force_mode16 = SB_MODE16_AUTO;
  } else if ( what == 1 ) {
    codec -> force_mode16 = SB_MODE16_PLAYBACK;
  } else if ( what == 2 ) {
    codec -> force_mode16 = SB_MODE16_RECORD;
  }
}

static void snd_sb16_proc_write( snd_info_buffer_t *buffer, void *private_data )
{
  sbdsp_t *codec;
  char line[ 80 ], str[ 80 ];
  char *ptr;
  
  codec = (sbdsp_t *)private_data;
  if ( snd_info_get_line( buffer, line, sizeof( line ) ) ) {
    buffer -> error = -EINVAL;
    return;
  }
  if ( !strncmp( line, "Playback ", 9 ) ) {
    ptr = snd_info_get_str( str, line + 9, sizeof( str ) );
    if ( !strncmp( str, "16", 2 ) ) snd_sb16_proc_setup_mode( codec, 1 ); else
    if ( !strncmp( str, "8", 1 ) ) snd_sb16_proc_setup_mode( codec, 2 ); else
    snd_sb16_proc_setup_mode( codec, 0 );
  } else if ( !strncmp( line, "Record ", 7 ) ) {
    ptr = snd_info_get_str( str, line + 7, sizeof( str ) );
    if ( !strncmp( str, "16", 2 ) ) snd_sb16_proc_setup_mode( codec, 2 ); else
    if ( !strncmp( str, "8", 1 ) ) snd_sb16_proc_setup_mode( codec, 1 ); else
    snd_sb16_proc_setup_mode( codec, 0 );
  } else {
    buffer -> error = -EINVAL;
  }
}
 
void snd_sb16_proc_init( snd_pcm_t *pcm )
{
  sbdsp_t *codec;
  snd_info_entry_t *entry;
  
  codec = (sbdsp_t *)pcm -> private_data;
  if ( (entry = snd_info_create_entry( pcm -> card, "sb16" )) != NULL ) {
    entry -> private_data = codec;
    entry -> mode = S_IFREG | S_IRUGO | S_IWUSR;
    entry -> t.text.read_size = 256;
    entry -> t.text.read = snd_sb16_proc_read;
    entry -> t.text.write_size = 128;
    entry -> t.text.write = snd_sb16_proc_write;
    if ( snd_info_register( entry ) < 0 ) {
      snd_info_free_entry( entry );
      entry = NULL;
    }
  }
  codec -> proc_entry = entry;
}

void snd_sb16_proc_done( snd_pcm_t *pcm )
{
  sbdsp_t *codec;
  
  codec = (sbdsp_t *)pcm -> private_data;
  if ( codec -> proc_entry ) {
    snd_info_unregister( codec -> proc_entry );
    codec -> proc_entry = NULL;
  }
}
