/*
 *  Digital Audio (PCM) abstract layer
 *  Copyright (c) by Jaroslav Kysela <perex@jcu.cz>
 *
 *
 *   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_MAIN_OBJECT_FILE
#include "driver.h"
#include "minors.h"
#include "pcm.h"
#include "control.h"
#include "ulaw.h"

snd_pcm_t *snd_pcm_devices[ SND_CARDS * SND_PCM_DEVICES ];
int (*snd_pcm_oss_register)( unsigned short minor ) = NULL;
int (*snd_pcm_oss_unregister)( unsigned short minor ) = NULL;

#if 0
#define SND_PCM_DEBUG_BUFFERS
#endif

#if 0	/* not used now - should be used only for debugging */
#define SND_PCM_SKIP_LOCKED_BLOCKS
#endif

static void snd_pcm_init_blocks( snd_pcm_t *pcm, int direction );

snd_mutex_define_static( register );

/*
 *
 */

int snd_pcm_dma_alloc( snd_pcm_t *pcm, int direction, int dmanum, char *ident )
{
  int err;
  snd_pcm_channel_t *pchn;
  struct snd_stru_dma *pdma;

  if ( (err = snd_dma_malloc( pcm -> card, dmanum, ident, 0 )) < 0 )
    return err;
  pchn = direction == SND_PCM_PLAYBACK ? &pcm -> playback : &pcm -> record;
  pdma = pcm -> card -> dmas[ dmanum ];
  pchn -> size = pdma -> size;
  pchn -> buffer = pdma -> buf;
  pchn -> dmanum = dmanum;
  pchn -> frag_size = 0;
  if ( pchn -> size < 4096 ) {
    snd_printd( "Invalid audio DMA size - %i\n", pchn -> size );
    snd_dma_free( pcm -> card, dmanum, 0 );
    return -ENOMEM;
  }
  pchn -> flags |= SND_PCM_FLG_DMAOK;
  return 0;
}

int snd_pcm_dma_free( snd_pcm_t *pcm, int direction, int dmanum )
{
  snd_pcm_channel_t *pchn;

  snd_dma_free( pcm -> card, dmanum, 0 );
  pchn = direction == SND_PCM_PLAYBACK ? &pcm -> playback : &pcm -> record;
  pchn -> flags &= ~SND_PCM_FLG_DMAOK;
  return 0;
}

void snd_pcm_clear_channel( snd_pcm_channel_t *pchn )
{
  pchn -> voices = 1;
  pchn -> mode = 0;
  pchn -> format = 0;
  pchn -> rate = 0;
  pchn -> real_rate = 0;
  pchn -> requested_block_size = 0;
  pchn -> requested_blocks = 0;
  pchn -> requested_subdivision = 0;
  pchn -> processed_bytes = 0;
  pchn -> interrupts = 0;
  pchn -> xruns = 0;
  pchn -> flags = 0;
  pchn -> used_size = 0;
  pchn -> mmap_size = 0;
  pchn -> neutral_byte = 0;
  pchn -> size = 0;
  pchn -> buffer = NULL;
  pchn -> dmanum = -1;
  pchn -> blocks = 0;
  pchn -> block_size = 0;
  pchn -> used = 0;
  pchn -> frag_size = 0;
  pchn -> head = 0;
  pchn -> tail = 0;
  pchn -> blocks_max = 0;
  pchn -> blocks_room = 0;
  pchn -> blocks_min = 0;
  snd_pcm_clear_time( pchn );
  snd_pcm_lockzero( pchn );
}

void snd_pcm_fill_with_neutral( snd_pcm_t *pcm, snd_pcm_channel_t *pchn )
{
  unsigned int size;
  
  size = pchn -> used_size;
  if ( pchn -> hw.dma_neutral ) {	/* lowlevel driver does size conversion */
    pchn -> hw.dma_neutral( pcm, pchn -> buffer, 0, size, pchn -> neutral_byte );
    pchn -> flags &= ~SND_PCM_FLG_NEUTRAL;
    return;
  }
  if ( pchn -> hw.flags & SND_PCM_HW_8BITONLY ) {
    if ( pchn -> mode & SND_PCM_MODE_16 ) size >>= 1;
  }
  if ( pchn -> hw.flags & SND_PCM_HW_16BITONLY ) {
    if ( !(pchn -> mode & SND_PCM_MODE_16) ) size <<= 1;
  }
#ifdef SNDCFG_DEBUG
  if ( size > pchn -> size ) {
    snd_printk( "pcm: Oops, neutral fill size %i > DMA size %i\n", size, pchn -> size );
    size = pchn -> size;
  }
#endif
  memset( pchn -> buffer, pchn -> neutral_byte, size );
  pchn -> flags &= ~SND_PCM_FLG_NEUTRAL;
}

/*
 *
 */

unsigned short snd_pcm_file_flags( unsigned short f_flags )
{
  switch ( f_flags & O_ACCMODE ) {
    case O_WRONLY:	return SND_PCM_LFLG_PLAY;
    case O_RDONLY:	return SND_PCM_LFLG_RECORD;
    default:		return SND_PCM_LFLG_BOTH;
  }
}

/*
 *  some shared routines for lowlevel driver
 */

void snd_pcm_playback_dma( snd_pcm_t *pcm,
			   unsigned char *buffer, unsigned int offset,
			   unsigned char *user, unsigned int count )
{
  copy_from_user( &buffer[ offset ], user, count );
}

void snd_pcm_playback_dma_ulaw( snd_pcm_t *pcm,
			        unsigned char *buffer, unsigned int offset,
			        unsigned char *user, unsigned int count )
{
  if ( pcm -> playback.mode & SND_PCM_MODE_ULAW )
    snd_translate_from_user( snd_ulaw_dsp, &buffer[ offset ], user, count );
   else
    copy_from_user( &buffer[ offset ], user, count );
}

void snd_pcm_playback_dma_neutral( snd_pcm_t *pcm,
				   unsigned char *buffer, unsigned int offset,
				   unsigned int count,
				   unsigned char neutral_byte )
{
  memset( &buffer[ offset ], neutral_byte, count );
}

void snd_pcm_record_dma( snd_pcm_t *pcm,
			 unsigned char *buffer, unsigned int offset,
			 unsigned char *user, unsigned int count )
{
  copy_to_user( user, &buffer[ offset ], count );
}

void snd_pcm_record_dma_ulaw( snd_pcm_t *pcm,
			      unsigned char *buffer, unsigned int offset,
			      unsigned char *user, unsigned int count )
{
  if ( pcm -> record.mode & SND_PCM_MODE_ULAW )
    snd_translate_to_user( snd_dsp_ulaw, user, &buffer[ offset ], count );
   else
    copy_to_user( user, &buffer[ offset ], count );
}

void snd_pcm_dma_move( snd_pcm_t *pcm,
                       unsigned char *buffer,
                       unsigned int dest_offset, unsigned int src_offset,
                       unsigned int count )
{
  memcpy( &buffer[ dest_offset ], &buffer[ src_offset ], count );
}

/*
 *  interrupt callbacks from lowlevel driver
 */

static inline int snd_pcm_playback_ok( snd_pcm_t *pcm )
{
  unsigned long flags;
  int result;

  snd_spin_lock( pcm, playback, &flags );
  result = pcm -> playback.used <= pcm -> playback.blocks_max &&
           pcm -> playback.blocks - pcm -> playback.used >= pcm -> playback.blocks_room;
  snd_spin_unlock( pcm, playback, &flags );
  return result;
}

static inline int snd_pcm_record_ok( snd_pcm_t *pcm )
{
  unsigned long flags;
  int result;

  snd_spin_lock( pcm, record, &flags );
  result = pcm -> record.used >= pcm -> record.blocks_min;
  snd_spin_unlock( pcm, record, &flags );
  return result;
}
 
static void snd_pcm_interrupt_playback( snd_pcm_t *pcm )
{
  unsigned long flags;
  snd_pcm_channel_t *pchn;
  int trigger = 0;
  
  pchn = &pcm -> playback;
  if ( !(pchn -> flags & SND_PCM_FLG_TRIGGER) ) {
    snd_printd( "snd_pcm_interrupt_playback: Oops, playback interrupt when playback isn't active!!!\n" );
    return;
  }
  if ( pchn -> used <= 0 ) {
    snd_printd( "snd_pcm_interrupt_playback: Internal error (1)\n" );
    return;
  }
  if ( pchn -> hw.flags & SND_PCM_HW_AUTODMA ) {
    /* try clear first few bytes from next block */
    pchn -> hw.dma_neutral( pcm, pchn -> buffer, pchn -> tail * pchn -> block_size, 16, pchn -> neutral_byte );
  }
  if ( pchn -> used == 1 ) {
    if ( !(pchn -> flags & SND_PCM_FLG_SYNC) ) {
#ifdef SND_PCM_DEBUG_BUFFERS
      printk( "playback: underrun!!! - jiffies = %li\n", jiffies );
#endif
      pchn -> total_xruns++;
      pchn -> xruns++;
    }
  }
  snd_spin_lock( pcm, playback, &flags );
  pchn -> interrupts++;
  pchn -> processed_bytes += pchn -> block_size;
  pchn -> tail++;
  pchn -> tail %= pchn -> blocks;
  pchn -> used--;
  if ( pchn -> used > 0 ) {
    snd_pcm_lock( pchn, pchn -> tail );
    snd_spin_unlock( pcm, playback, &flags );
  } else {
    snd_pcm_lockzero( pchn );
    pchn -> head = pchn -> tail = 0;
    snd_spin_unlock( pcm, playback, &flags );
    if ( pchn -> frag_size > 0 ) {
      pchn -> hw.dma_move( pcm, pchn -> buffer, 0, pchn -> head * pchn -> block_size, pchn -> frag_size );
    }
  }
#if 0
  printk( "interrupt!!! tail = %i, used = %i\n", pchn -> tail, pchn -> used );
#endif
  if ( pchn -> used > 0 ) trigger = 1;
  if ( trigger && !(pchn -> hw.flags & SND_PCM_HW_AUTODMA) ) {
    pchn -> hw.prepare( pcm,
            		pchn -> buffer, pchn -> used_size,            			
            		pchn -> tail * pchn -> block_size,
                        pchn -> block_size );
    pchn -> hw.trigger( pcm, 1 );
  }
  if ( !trigger ) {
    if ( pchn -> hw.flags & SND_PCM_HW_AUTODMA ) {
      pchn -> hw.trigger( pcm, 0 );
      snd_pcm_clear_time( pchn );
    }
    pchn -> flags &= ~SND_PCM_FLG_TRIGGER;
  }
  if ( (pchn -> flags & SND_PCM_FLG_SLEEP) && snd_pcm_playback_ok( pcm ) )
    snd_wakeup( pcm, playback );
}

static void snd_pcm_interrupt_record( snd_pcm_t *pcm )
{
  unsigned long flags;
  snd_pcm_channel_t *pchn;
  int trigger;
  
  pchn = &pcm -> record;
  if ( !(pchn -> flags & SND_PCM_FLG_TRIGGER) ) {
    snd_printd( "snd_pcm_interrupt_record: Oops, record interrupt when record isn't active!!!\n" );
    return;
  }
  trigger = 0;
  snd_spin_lock( pcm, record, &flags );
  if ( pchn -> used < pchn -> blocks ) {
    pchn -> used++;
  } else {
    pchn -> total_xruns++;
    pchn -> xruns++;
  }
  pchn -> interrupts++;
  pchn -> processed_bytes += pchn -> block_size;
  pchn -> head++;
  pchn -> head %= pchn -> blocks;
  snd_pcm_lock( pchn, pchn -> head );
  snd_spin_unlock( pcm, record, &flags );
  if ( !(pchn -> flags & SND_PCM_FLG_SYNC) ) trigger = 1;
  if ( trigger && !(pchn -> hw.flags & SND_PCM_HW_AUTODMA) ) {
    pchn -> hw.prepare( pcm,
            		pchn -> buffer, pchn -> used_size,            			
            		pchn -> head * pchn -> block_size,
                        pchn -> block_size );
    pchn -> hw.trigger( pcm, 1 );
  }
  if ( !trigger ) { 
    if ( pchn -> hw.flags & SND_PCM_HW_AUTODMA ) {
      pchn -> hw.trigger( pcm, 0 );
      snd_pcm_clear_time( pchn );
    }
    pchn -> flags &= ~SND_PCM_FLG_TRIGGER;
  }
  if ( (pchn -> flags & SND_PCM_FLG_SLEEP) && snd_pcm_record_ok( pcm ) )
    snd_wakeup( pcm, record );
}

/*
 *  trigger standard buffered playback
 */

static void snd_pcm_trigger_playback( snd_pcm_t *pcm, snd_pcm_channel_t *pchn )
{
  unsigned long flags;

  if ( !pchn -> used ) return;
  snd_spin_lock( pcm, playback, &flags );
  if ( !(pchn -> flags & SND_PCM_FLG_TRIGGER) ) {
    pchn -> flags |= SND_PCM_FLG_TRIGGERA;
    snd_pcm_lock( pchn, pchn -> tail );
    snd_spin_unlock( pcm, playback, &flags );
    pchn -> hw.prepare( pcm,
            		pchn -> buffer, pchn -> used_size,            			
            		pchn -> tail * pchn -> block_size,
                        pchn -> block_size );
    pchn -> hw.trigger( pcm, 1 );
    if ( pchn -> flags & SND_PCM_FLG_TIME ) {
      do_gettimeofday( &pchn -> time );
    }
  } else {
    snd_spin_unlock( pcm, playback, &flags );
  }
}

/*
 *  user to dma
 */

static int snd_pcm_user_to_buffer( struct file *file, snd_pcm_t *pcm, const char *buf, int count )
{
  unsigned long flags;
  snd_pcm_channel_t *pchn;
  int result, tmp;

  if ( count <= 0 || !buf ) return 0;
  if ( verify_area( VERIFY_READ, buf, count ) ) return -EFAULT;

  pchn = &pcm -> playback;

#if 0
  snd_printk( "pcm_user_to_dma: buf=0x%x, count=0x%x,%i (%s), used = %i, jiffies = %li\n", (int)buf, count, count, file -> f_flags & O_NONBLOCK ? "non blocked" : "blocked", pchn -> used, jiffies );
#endif
  count &= ~( (pchn -> voices * (pchn -> mode & SND_PCM_MODE_16 ? 2 : 1)) - 1 );
  pchn -> flags &= ~SND_PCM_FLG_ABORT;
  if ( pchn -> flags & SND_PCM_FLG_NEUTRAL )
    snd_pcm_fill_with_neutral( pcm, pchn );

  result = 0;

  /*
   * OK. This is patch for .au files which begin with .snd header...
   * This is a little bit hard - it's application work to do conversions...
   */
  if ( (pchn -> mode & SND_PCM_MODE_ULAW) && !pchn -> processed_bytes && !pchn -> used && count > 31 ) {
    unsigned char buffer[ 8 ];
      
    copy_from_user( buffer, buf, 8 );
    if ( !memcmp( buffer, ".snd", 4 ) ) {
      tmp = (buffer[4]<<24)|(buffer[5]<<16)|(buffer[6]<<8)|(buffer[7]);
      if ( tmp > count ) tmp = count;
      if ( tmp < 128 ) {
         buf += tmp;
         count -= tmp;
         result += tmp;
      }
    }
  }

  while ( count > 0 )
    {
      while ( !snd_pcm_playback_ok( pcm ) )
        {
          if ( !(pchn -> flags & SND_PCM_FLG_TRIGGER) )
            snd_pcm_trigger_playback( pcm, pchn );
          if ( file -> f_flags & O_NONBLOCK ) return result;
          if ( !(pchn -> flags & SND_PCM_FLG_ENABLE) ) return result;
          snd_spin_lock( pcm, playback_sleep, &flags );
          pchn -> flags |= SND_PCM_FLG_SLEEP;
          snd_sleep( pcm, playback, 10 * HZ );
          pchn -> flags &= ~SND_PCM_FLG_SLEEP;
          snd_spin_unlock( pcm, playback_sleep, &flags );
          if ( snd_sleep_abort( pcm, playback ) )
            {
              pchn -> flags |= SND_PCM_FLG_ABORT;
              return result;
            }
          if ( pchn -> used >= pchn -> blocks && snd_timeout( pcm, playback ) )
            {
              snd_printd( "pcm_user_to_dma: timeout, new block discarded\n" );
              return -EIO;
            }
        }
        
#ifdef SND_PCM_SKIP_LOCKED_BLOCKS
      /* ok.. interresant part.. we must find first free & not locked block.. */        
      tmp = 0;
      snd_spin_lock( pcm, playback, &flags );
      while ( snd_pcm_islock( pchn, pchn -> head ) )
        {
          pchn -> hw.discarded++;
#ifdef SND_PCM_DEBUG_BUFFERS
          printk( "playback: discarded!!! %i - %i\n", pchn -> block_lock, pchn -> head );
#endif
          pchn -> used++;
          pchn -> head++;
          pchn -> head %= pchn -> blocks;
          if ( pchn -> used >= pchn -> blocks )
            {
              tmp++;
              break;
            }
        }
      snd_spin_unlock( pcm, playback, &flags );
      if ( tmp ) continue;		/* go to sleep loop */
#endif

      /* ok.. now we have right block - fill it */
      
      tmp = pchn -> block_size - pchn -> frag_size;
      if ( tmp > count ) tmp = count;	/* correction */

#if 0
      snd_printk( "to dma: buf = 0x%lx, size = %i\n", (long)buf, tmp );
#endif
      if ( verify_area( VERIFY_READ, buf, tmp ) ) return -EFAULT;
      pchn -> hw.dma( pcm,
                      pchn -> buffer,
                      ( pchn -> head * pchn -> block_size ) + pchn -> frag_size,
                      (char *)buf, tmp );
      snd_pcm_proc_write( pchn, buf, tmp );

      count -= tmp;
      result += tmp;
      buf += tmp;

      snd_spin_lock( pcm, playback, &flags );
      pchn -> frag_size += tmp;
      if ( pchn -> frag_size >= pchn -> block_size )
        {
#if 0
          printk( "block filled = %i, used = %i, blocks = %i\n", pchn -> head, pchn -> used, pchn -> blocks );
#endif
          pchn -> used++;
          pchn -> head++;
          pchn -> head %= pchn -> blocks;
          pchn -> frag_size = 0;
        }
      snd_spin_unlock( pcm, playback, &flags );
    }
#if 0
  snd_printk( "pcm_user_to_dma: end\n" );
#endif
  if ( pchn -> used )
    snd_pcm_trigger_playback( pcm, pchn );
  return result;
}

/*
 *  trigger standard buffered record
 */

static void snd_pcm_trigger_record( snd_pcm_t *pcm, snd_pcm_channel_t *pchn )
{
  unsigned long flags;

  if ( pchn -> used >= pchn -> blocks ) return;
  snd_spin_lock( pcm, record, &flags );
  if ( !(pchn -> flags & SND_PCM_FLG_TRIGGER) ) {
    pchn -> flags |= SND_PCM_FLG_TRIGGERA;
    snd_pcm_lock( pchn, pchn -> head );
    snd_spin_unlock( pcm, record, &flags );
    pchn -> hw.prepare( pcm,
            		pchn -> buffer, pchn -> used_size,            			
            		pchn -> head * pchn -> block_size,
                        pchn -> block_size );
    pchn -> hw.trigger( pcm, 1 );
    if ( pchn -> flags & SND_PCM_FLG_TIME ) {
      do_gettimeofday( &pchn -> time );
    }
  } else {
    snd_spin_unlock( pcm, record, &flags );
  }
}

/*
 *  dma to user
 */

static int snd_pcm_buffer_to_user( struct file *file, snd_pcm_t *pcm, char *buf, int count )
{
  unsigned long flags;
  snd_pcm_channel_t *pchn;
  int result, tmp;

  if ( count <= 0 ) return 0;
  if ( verify_area( VERIFY_WRITE, buf, count ) ) return -EFAULT;
  
  pchn = &pcm -> record;
#if 0
  snd_printk( "pcm_user_from_dma: buf=0x%x, count=0x%x (%s)\n", (int)buf, count, file -> f_flags & O_NONBLOCK ? "non blocked" : "blocked" );
#endif
  count &= ~( (pchn -> voices * (pchn -> mode & SND_PCM_MODE_16 ? 2 : 1)) - 1 );
  pchn -> flags &= ~SND_PCM_FLG_ABORT;
  if ( pchn -> flags & SND_PCM_FLG_NEUTRAL )
    snd_pcm_fill_with_neutral( pcm, pchn );

  if ( !(pchn -> flags & SND_PCM_FLG_ENABLE) ) return 0;
  snd_pcm_trigger_record( pcm, pchn );
  result = 0;
  
  while ( count > 0 )
    {
      while ( !snd_pcm_record_ok( pcm ) )
        {
          if ( file -> f_flags & O_NONBLOCK ) return result;
          snd_spin_lock( pcm, record_sleep, &flags );
          pchn -> flags |= SND_PCM_FLG_SLEEP;
          snd_sleep( pcm, record, 10 * HZ );
          pchn -> flags &= ~SND_PCM_FLG_SLEEP;
          snd_spin_unlock( pcm, record_sleep, &flags );
          if ( snd_sleep_abort( pcm, record ) )
            {
              pchn -> flags |= SND_PCM_FLG_ABORT;
              return -EINTR;
            }
          if ( !pchn -> used && snd_timeout( pcm, record ) )
            {
              snd_printd( "snd_pcm_dma_to_user: data timeout\n" );
              return -EIO;
            }
        }

#ifdef SND_PCM_SKIP_LOCKED_BLOCKS
      tmp = 0;
      snd_spin_lock( pcm, record, &flags );
      while ( snd_pcm_islock( pchn, pchn -> tail ) )
        {
          pchn -> hw.discarded++;
          pchn -> used--;
          pchn -> tail++;
          pchn -> tail %= pchn -> blocks;
          if ( !pchn -> used )
            {
              tmp++;
              break;
            }
        }
      snd_spin_unlock( pcm, record, &flags );
      if ( tmp ) continue;		/* go to sleep loop */
#endif
        
      tmp = count <= pchn -> frag_size ? count : pchn -> frag_size;
      if ( verify_area( VERIFY_WRITE, buf, tmp ) ) return -EFAULT;
      pchn -> hw.dma( pcm,
                      pchn -> buffer,
      		      ( pchn -> tail * pchn -> block_size ) + ( pchn -> block_size - pchn -> frag_size ),
                      buf, tmp );
      if ( !verify_area( VERIFY_READ, buf, tmp ) )
        snd_pcm_proc_write( pchn, buf, tmp );

      buf += tmp;
      count -= tmp;
      result += tmp;

      snd_spin_lock( pcm, record, &flags );
      pchn -> frag_size -= tmp;
      if ( !pchn -> frag_size )
        {
          pchn -> used--;
          pchn -> tail++;
          pchn -> tail %= pchn -> blocks;
          pchn -> frag_size = pchn -> block_size;
        }
      snd_spin_unlock( pcm, record, &flags );
    }

  return result;
}

/*
 *  synchronize playback
 */

static int snd_pcm_drain_playback( snd_pcm_t *pcm )
{
  unsigned long flags;
  snd_pcm_channel_t *pchn;

#ifdef SND_PCM_DEBUG_BUFFERS
  snd_printk( "drain playback!!!\n" );
#endif
  pchn = &pcm -> playback;
  snd_spin_lock( pcm, playback, &flags );
  if ( pchn -> flags & SND_PCM_FLG_TRIGGER ) {
    pchn -> hw.trigger( pcm, 0 );
    snd_pcm_clear_time( pchn );
    pchn -> flags &= ~SND_PCM_FLG_TRIGGER;
  }
  snd_spin_unlock( pcm, playback, &flags );
  pchn -> flags |= SND_PCM_FLG_NEUTRAL;
  snd_pcm_fill_with_neutral( pcm, pchn );
  snd_spin_lock( pcm, playback, &flags );
  pchn -> flags &= ~SND_PCM_FLG_TRIGGER1;
  pchn -> head = pchn -> tail = pchn -> used = 0;
  pchn -> frag_size = 0;
  snd_pcm_lockzero( pchn );
  snd_spin_unlock( pcm, playback, &flags );
  return 0;
}

static int snd_pcm_flush_playback( snd_pcm_t *pcm )
{
  int err = 0;
  unsigned long flags;
  snd_pcm_channel_t *pchn;

#ifdef SND_PCM_DEBUG_BUFFERS
  snd_printk( "flush playback!!!\n" );
#endif
  pchn = &pcm -> playback;
  if ( pchn -> flags & SND_PCM_FLG_ABORT ) {
    snd_pcm_drain_playback( pcm );
    return 0;
  }
  snd_spin_lock( pcm, playback, &flags );
  pchn -> flags |= SND_PCM_FLG_SYNC;
  if ( pchn -> used < pchn -> blocks && pchn -> frag_size > 0 ) {
    unsigned int size = pchn -> frag_size;
    unsigned int count = pchn -> block_size - size;
    unsigned int offset;
      
    pchn -> used++;
    pchn -> head++;
    pchn -> head %= pchn -> blocks;
    snd_spin_unlock( pcm, playback, &flags );
    if ( count > 0 ) {
      offset = ( pchn -> head * pchn -> block_size ) + size;
      pchn -> hw.dma_neutral( pcm, pchn -> buffer, offset, count, pchn -> neutral_byte );
    }
  } else {
    snd_spin_unlock( pcm, playback, &flags );
  }
  if ( !pchn -> used ) {
    pchn -> flags &= ~SND_PCM_FLG_SYNC;
    snd_pcm_drain_playback( pcm );
    return 0;
  }

  snd_pcm_trigger_playback( pcm, pchn );

  while ( pchn -> used ) {
    snd_spin_lock( pcm, playback_sleep, &flags );
    pchn -> flags |= SND_PCM_FLG_SLEEP;
    snd_sleep( pcm, playback, 10 * HZ );
    pchn -> flags &= ~SND_PCM_FLG_SLEEP;
    snd_spin_unlock( pcm, playback_sleep, &flags );
    if ( snd_sleep_abort( pcm, playback ) ) {
      pchn -> flags |= SND_PCM_FLG_ABORT;
      err = -EINTR;
      break;
    }
    if ( pchn -> used && snd_timeout( pcm, playback ) ) {
      snd_printd( "snd_pcm_flush_playback: timeout, skipping waiting blocks\n" );
      err = -EAGAIN;
      break;
    }
  }
  pchn -> flags &= ~SND_PCM_FLG_SYNC;
  snd_pcm_drain_playback( pcm );  
  return err;
}

static int snd_pcm_drain_record( snd_pcm_t *pcm )
{
  unsigned long flags;
  snd_pcm_channel_t *pchn;

#ifdef SND_PCM_DEBUG_BUFFERS
  snd_printk( "drain record!!!\n" );
#endif
  pchn = &pcm -> record;
  snd_spin_lock( pcm, record, &flags );
  if ( pchn -> flags & SND_PCM_FLG_TRIGGER ) {
    pchn -> hw.trigger( pcm, 0 );
    snd_pcm_clear_time( pchn );
    pchn -> flags &= ~SND_PCM_FLG_TRIGGER;
  }
  snd_spin_unlock( pcm, record, &flags );
  pchn -> flags |= SND_PCM_FLG_NEUTRAL;
  snd_pcm_fill_with_neutral( pcm, pchn );
  snd_spin_lock( pcm, record, &flags );
  pchn -> flags &= ~SND_PCM_FLG_TRIGGER1;
  pchn -> head = pchn -> tail = pchn -> used = 0;
  pchn -> frag_size = pchn -> block_size;
  snd_pcm_lockzero( pchn );
  snd_spin_unlock( pcm, record, &flags );
  return 0;
}

static int snd_pcm_flush_record( snd_pcm_t *pcm )
{
  int err = 0;
  unsigned long flags;
  snd_pcm_channel_t *pchn;

#ifdef SND_PCM_DEBUG_BUFFERS
  snd_printk( "flush record!!!\n" );
#endif
  pchn = &pcm -> record;
  pchn -> flags |= SND_PCM_FLG_SYNC;
  while ( pchn -> flags & SND_PCM_FLG_TRIGGER ) {	/* still recording? */
    snd_spin_lock( pcm, record_sleep, &flags );
    pchn -> flags |= SND_PCM_FLG_SLEEP;
    snd_sleep( pcm, record, 10 * HZ );
    pchn -> flags &= ~SND_PCM_FLG_SLEEP;
    snd_spin_unlock( pcm, record_sleep, &flags );
    if ( snd_sleep_abort( pcm, record ) ) {
      pchn -> flags |= SND_PCM_FLG_ABORT;
      err = -EINTR;
      break;
    }
    if ( (pchn -> flags & SND_PCM_FLG_TRIGGER) && snd_timeout( pcm, record ) ) {
      snd_printd( "snd_pcm_flush_record: sync timeout\n" );
      err = -EAGAIN;
      break;
    }    
  }
  pchn -> flags &= ~SND_PCM_FLG_SYNC;
  snd_pcm_drain_record( pcm );
  return 0;
}

/*
 *  other things
 */

static unsigned int snd_pcm_align( snd_pcm_t *pcm, int direction, unsigned int value )
{
  snd_pcm_channel_t *pchn;

  pchn = direction == SND_PCM_PLAYBACK ? &pcm -> playback : &pcm -> record;
  value &= ~(pchn -> voices * (pchn -> mode & SND_PCM_MODE_16 ? 2 : 1));
  value &= ~pchn -> hw.align;
  if ( !value ) value = pchn -> hw.align + 1;
  return value;
}

static void snd_pcm_init_blocks( snd_pcm_t *pcm, int direction )
{
  unsigned int block, blocks, bps, min;
  snd_pcm_channel_t *pchn;
  unsigned long flags;

  pchn = direction == SND_PCM_PLAYBACK ? &pcm -> playback : &pcm -> record;
  /* try make fragment size max 1sec long */
  if ( direction == SND_PCM_PLAYBACK ) {
    snd_spin_lock( pcm, playback, &flags );
  } else {
    snd_spin_lock( pcm, record, &flags );
  } 
  pchn -> used_size = pchn -> size;
  if ( pchn -> hw.flags & SND_PCM_HW_8BITONLY ) {
    if ( pchn -> mode & SND_PCM_MODE_16 ) pchn -> used_size <<= 1;
  }
  if ( pchn -> hw.flags & SND_PCM_HW_16BITONLY ) {
    if ( !(pchn -> mode & SND_PCM_MODE_16) ) pchn -> used_size >>= 1;
  }                    
  for ( min = 1, bps = pchn -> hw.min_fragment; bps > 1; bps-- ) min <<= 1;
  bps = pchn -> rate * pchn -> voices;
  if ( pchn -> mode & SND_PCM_MODE_16 ) bps <<= 1;
  if ( pchn -> mode & SND_PCM_MODE_ADPCM ) bps >>= 2;
  block = pchn -> used_size;
  while ( block > bps ) block >>= 1;
  if ( block == pchn -> used_size ) block >>= 1;
  if ( direction == SND_PCM_RECORD )
    block /= 4;			/* small fragment when recording */
  block = snd_pcm_align( pcm, direction, block );
  if ( block < min ) block = min;
  blocks = pchn -> used_size / block;
  pchn -> used_size = blocks * block;
  pchn -> blocks = blocks;
  pchn -> block_size = block;
  pchn -> flags |= SND_PCM_FLG_NEUTRAL;
  if ( direction == SND_PCM_RECORD )
    pchn -> frag_size = pchn -> block_size;
  pchn -> blocks_max = blocks - 1;
  pchn -> blocks_room = 1;
  pchn -> blocks_min = 1;
  if ( direction == SND_PCM_PLAYBACK ) {
    snd_spin_unlock( pcm, playback, &flags );
  } else {
    snd_spin_unlock( pcm, record, &flags );
  }
#ifdef SND_PCM_DEBUG_BUFFERS
  snd_printk( "used_size = %i, blocks = %i, blocks_size = %i\n", pchn -> used_size, pchn -> blocks, pchn -> block_size );
#endif
}

static int snd_pcm_info( snd_pcm_t *pcm, snd_pcm_info_t *xinfo )
{
  snd_pcm_info_t ginfo, *info;

  info = xinfo;
  if ( verify_area( VERIFY_WRITE, info, sizeof( snd_pcm_info_t ) ) ) return -EFAULT;
  info = &ginfo;
  memset( &ginfo, 0, sizeof( ginfo ) );
  ginfo.type = pcm -> card -> type;
  ginfo.flags = pcm -> info_flags;
  strncpy( ginfo.id, pcm -> id, sizeof( ginfo.id ) - 1 );
  strncpy( ginfo.name, pcm -> name, sizeof( ginfo.name ) - 1 );
  copy_to_user( xinfo, &ginfo, sizeof( ginfo ) );
  return 0;
}

static int snd_pcm_playback_info( snd_pcm_t *pcm, snd_pcm_playback_info_t *xinfo )
{
  snd_pcm_playback_info_t ginfo, *info;
  snd_pcm_channel_t *pchn;
  int i;

  info = xinfo;
  if ( verify_area( VERIFY_WRITE, xinfo, sizeof( snd_pcm_playback_info_t ) ) ) return -EFAULT;
  info = &ginfo;
  memset( &ginfo, 0, sizeof( ginfo ) );
  pchn = &pcm -> playback;
  ginfo.flags = pchn -> hw.flags & SND_PCM_HW_COPYMASK;
  ginfo.formats = pchn -> hw.formats;
  ginfo.min_rate = pchn -> hw.min_rate;
  ginfo.max_rate = pchn -> hw.max_rate;
  ginfo.min_channels = 1;
  ginfo.max_channels = pchn -> hw.max_voices;
  ginfo.buffer_size = pchn -> size;
  if ( pchn -> hw.flags & SND_PCM_HW_8BITONLY ) {
    if ( pchn -> mode & SND_PCM_MODE_16 ) ginfo.buffer_size <<= 1;
  }
  if ( pchn -> hw.flags & SND_PCM_HW_16BITONLY ) {
    if ( !(pchn -> mode & SND_PCM_MODE_16) ) ginfo.buffer_size >>= 1;
  }                    
  ginfo.min_fragment_size = 1;
  for ( i = 1; i < pchn -> hw.min_fragment; i++ ) ginfo.min_fragment_size <<= 1;
  ginfo.max_fragment_size = ginfo.buffer_size / 2;
  ginfo.fragment_align = pchn -> hw.align;
  copy_to_user( xinfo, &ginfo, sizeof( ginfo ) );
  return 0;
}

static int snd_pcm_record_info( snd_pcm_t *pcm, snd_pcm_record_info_t *xinfo )
{
  snd_pcm_record_info_t ginfo, *info;
  snd_pcm_channel_t *pchn;
  int i;

  info = xinfo;
  if ( verify_area( VERIFY_WRITE, xinfo, sizeof( snd_pcm_record_info_t ) ) ) return -EFAULT;
  info = &ginfo;
  memset( &ginfo, 0, sizeof( ginfo ) );
  pchn = &pcm -> record;
  ginfo.flags = pchn -> hw.flags & SND_PCM_HW_COPYMASK;
  ginfo.formats = pchn -> hw.formats;
  ginfo.min_rate = pchn -> hw.min_rate;
  ginfo.max_rate = pchn -> hw.max_rate;
  ginfo.min_channels = 1;
  ginfo.max_channels = pchn -> hw.max_voices;
  ginfo.buffer_size = pchn -> size;
  if ( pchn -> hw.flags & SND_PCM_HW_8BITONLY ) {
    if ( pchn -> mode & SND_PCM_MODE_16 ) ginfo.buffer_size <<= 1;
  }
  if ( pchn -> hw.flags & SND_PCM_HW_16BITONLY ) {
    if ( !(pchn -> mode & SND_PCM_MODE_16) ) ginfo.buffer_size >>= 1;
  }                    
  ginfo.min_fragment_size = 1;
  for ( i = 1; i < pchn -> hw.min_fragment; i++ ) ginfo.min_fragment_size <<= 1;
  ginfo.max_fragment_size = ginfo.buffer_size / 2;
  ginfo.fragment_align = pchn -> hw.align;
  copy_to_user( xinfo, &ginfo, sizeof( ginfo ) );
  return 0;
}

static int snd_pcm_format( snd_pcm_t *pcm, int direction, snd_pcm_format_t *_format )
{
  int err = 0;
  snd_pcm_format_t format;
  snd_pcm_channel_t *pchn;

  if ( verify_area( VERIFY_READ, _format, sizeof( format ) ) ) return -EFAULT;
  copy_from_user( &format, _format, sizeof( format ) );
  pchn = direction == SND_PCM_PLAYBACK ? &pcm -> playback : &pcm -> record;
  if ( format.rate < pchn -> hw.min_rate ) format.rate = pchn -> hw.min_rate;
  if ( format.rate > pchn -> hw.max_rate ) format.rate = pchn -> hw.max_rate;
  if ( format.channels < 1 ) {
    format.channels = 1;
    err = -EINVAL;
  }
  if ( format.channels > pchn -> hw.max_voices ) {
    format.channels = pchn -> hw.max_voices;
    return -EINVAL;
  }
  if ( !(pchn -> hw.formats & (1<<format.format)) ) {
    /* muLaw must be supported by all devices */
    if ( format.format != SND_PCM_SFMT_MU_LAW ) {
      err = -EINVAL;
    }
  }
  if ( direction == SND_PCM_PLAYBACK )
    snd_pcm_flush_playback( pcm );
   else
    snd_pcm_flush_record( pcm );
  pchn -> mode &= ~SND_PCM_MODE_TYPE;
  switch ( format.format ) {
    case SND_PCM_SFMT_MU_LAW:
      pchn -> mode |= SND_PCM_MODE_U | SND_PCM_MODE_ULAW;
      break;
    case SND_PCM_SFMT_A_LAW:
      pchn -> mode |= SND_PCM_MODE_U | SND_PCM_MODE_ALAW;
      break;
    case SND_PCM_SFMT_IMA_ADPCM:
      pchn -> mode |= SND_PCM_MODE_U | SND_PCM_MODE_ADPCM;
      break;
    case SND_PCM_SFMT_U8:
      pchn -> mode |= SND_PCM_MODE_U;
      break;
    case SND_PCM_SFMT_S16_LE:
      pchn -> mode |= SND_PCM_MODE_16;
      break;
    case SND_PCM_SFMT_S16_BE:
      pchn -> mode |= SND_PCM_MODE_16 | SND_PCM_MODE_BIG;
      break;
    case SND_PCM_SFMT_S8:
      break;
    case SND_PCM_SFMT_U16_LE:
      pchn -> mode |= SND_PCM_MODE_16 | SND_PCM_MODE_U;
      break;
    case SND_PCM_SFMT_U16_BE:
      pchn -> mode |= SND_PCM_MODE_16 | SND_PCM_MODE_BIG | SND_PCM_MODE_U;
      break;
    case SND_PCM_SFMT_MPEG:
      pchn -> mode |= SND_PCM_MODE_MPEG;
      break;
    case SND_PCM_SFMT_GSM:
      pchn -> mode |= SND_PCM_MODE_GSM;
      break;
    default:
      pchn -> mode |= SND_PCM_MODE_U | SND_PCM_MODE_ULAW;
      err = -EINVAL;
  }
  pchn -> rate = format.rate;
  pchn -> voices = format.channels;
  pchn -> hw.compute_rate( pcm );
  pchn -> flags |= SND_PCM_FLG_BUFFERS;
  pchn -> neutral_byte = pchn -> mode & SND_PCM_MODE_16 ? 0x00 : 0x80;
  return err;
}

static int snd_pcm_playback_params( snd_pcm_t *pcm, snd_pcm_playback_params_t *_params )
{
  snd_pcm_playback_params_t params;
  snd_pcm_channel_t *pchn;
  int i, tmp;
  unsigned long flags;

  if ( verify_area( VERIFY_READ, _params, sizeof( params ) ) ) return -EFAULT;
  copy_from_user( &params, _params, sizeof( params ) );
  snd_pcm_flush_playback( pcm );
  snd_spin_lock( pcm, playback, &flags );
  pchn = &pcm -> playback;
  for ( tmp = i = 1; i < pchn -> hw.min_fragment; i++ ) tmp <<= 1;
  if ( params.fragment_size < tmp || params.fragment_size > pchn -> size / 2 ) {
    snd_spin_unlock( pcm, playback, &flags );
    return -EINVAL;
  }
  pchn -> used_size = pchn -> size;
  if ( pchn -> hw.flags & SND_PCM_HW_8BITONLY ) {
    if ( pchn -> mode & SND_PCM_MODE_16 ) pchn -> used_size <<= 1;
  }
  if ( pchn -> hw.flags & SND_PCM_HW_16BITONLY ) {
    if ( !(pchn -> mode & SND_PCM_MODE_16) ) pchn -> used_size >>= 1;
  }                    
  pchn -> block_size = snd_pcm_align( pcm, SND_PCM_PLAYBACK, params.fragment_size );
  pchn -> blocks = pchn -> used_size / pchn -> block_size;
  pchn -> used_size = pchn -> blocks * pchn -> block_size;
  if ( params.fragments_max < 0 ) {
    pchn -> blocks_max = pchn -> blocks + params.fragments_max;
    if ( pchn -> blocks_max < 1 ) pchn -> blocks_max = 1;
  } else {
    pchn -> blocks_max = params.fragments_max;
    if ( pchn -> blocks_max > pchn -> blocks - 1 ) pchn -> blocks_max = pchn -> blocks - 1;
  }
  pchn -> blocks_room = params.fragments_room;
  if ( pchn -> blocks_room > pchn -> blocks / 2 ) pchn -> blocks_room = pchn -> blocks / 2;
  snd_spin_unlock( pcm, playback, &flags );
#ifdef SND_PCM_DEBUG_BUFFERS
  snd_printk( "playback_params: used_size = %i, blocks = %i, blocks_size = %i, blocks_max = %i, blocks_room = %i\n", pchn -> used_size, pchn -> blocks, pchn -> block_size, pchn -> blocks_max, pchn -> blocks_room );
#endif
  return 0;
}

static int snd_pcm_record_params( snd_pcm_t *pcm, snd_pcm_record_params_t *_params )
{
  snd_pcm_record_params_t params;
  snd_pcm_channel_t *pchn;
  int i, tmp;
  unsigned long flags;

  if ( verify_area( VERIFY_READ, _params, sizeof( params ) ) ) return -EFAULT;
  copy_from_user( &params, _params, sizeof( params ) );
  snd_pcm_flush_record( pcm );
  snd_spin_lock( pcm, record, &flags );
  pchn = &pcm -> record;
  for ( tmp = i = 1; i < pchn -> hw.min_fragment; i++ ) tmp <<= 1;
  pchn -> used_size = pchn -> size;
  if ( pchn -> hw.flags & SND_PCM_HW_8BITONLY ) {
    if ( pchn -> mode & SND_PCM_MODE_16 ) pchn -> used_size <<= 1;
  }
  if ( pchn -> hw.flags & SND_PCM_HW_16BITONLY ) {
    if ( !(pchn -> mode & SND_PCM_MODE_16) ) pchn -> used_size >>= 1;
  }                    
  if ( params.fragment_size < tmp || params.fragment_size > pchn -> size / 2 ) {
    snd_spin_unlock( pcm, record, &flags );
    return -EINVAL;
  }
  pchn -> block_size = snd_pcm_align( pcm, SND_PCM_RECORD, params.fragment_size );
  pchn -> blocks = pchn -> used_size / pchn -> block_size;
  pchn -> used_size = pchn -> blocks * pchn -> block_size;
  pchn -> blocks_min = params.fragments_min;
  if ( pchn -> blocks_min > pchn -> blocks / 2 ) pchn -> blocks_min = pchn -> blocks / 2;
  if ( pchn -> blocks_min < 1 ) pchn -> blocks_min = 1;
  snd_spin_unlock( pcm, record, &flags );
#ifdef SND_PCM_DEBUG_BUFFERS
  snd_printk( "record_params: used_size = %i, blocks = %i, blocks_size = %i, blocks_min = %i\n", pchn -> used_size, pchn -> blocks, pchn -> block_size, pchn -> blocks_min );
#endif
  return 0;
}

static void snd_pcm_get_time( snd_pcm_channel_t *pchn, struct timeval *time, int diff_bytes )
{
  unsigned int bps;		/* bytes per second */

  if ( pchn -> time.tv_sec == 0 ) {
    time -> tv_sec = time -> tv_usec = 0;
  } else {
    bps = pchn -> real_rate * pchn -> voices;
    if ( pchn -> mode & SND_PCM_MODE_16 ) bps <<= 1;
    if ( pchn -> mode & SND_PCM_MODE_ADPCM ) bps >>= 2;
    if ( bps == 0 ) {
      snd_printd( "Aiee, bps == 0, real_rate = %i, voices = %i\n", pchn -> real_rate, pchn -> voices );
      return;
    }
    do_gettimeofday( time );
    time -> tv_sec += diff_bytes / (int)bps;
    diff_bytes %= (int)bps;
    /* precision should be better :-((, but we have only 2^31 range */
    if ( diff_bytes > 214748 || diff_bytes < -214748 ) {
      time -> tv_usec += (((1000L * diff_bytes) + (int)(bps >> 1)) / (int)bps) * 1000L;
    } else {
      time -> tv_usec += (((10000L * diff_bytes) + (int)(bps >> 1)) / (int)bps) * 100L;
    }
    if ( time -> tv_usec < 0 ) {
      time -> tv_usec += 1000000UL;
      time -> tv_sec--;
    } else if ( time -> tv_usec >= 1000000UL ) {
      time -> tv_usec -= 1000000UL;
      time -> tv_sec++;
    }
  }
}

static int snd_pcm_playback_status( snd_pcm_t *pcm, snd_pcm_playback_status_t *_status )
{
  snd_pcm_playback_status_t status;
  snd_pcm_channel_t *pchn;
  unsigned long flags;
  int ptr;

  if ( verify_area( VERIFY_WRITE, _status, sizeof( status ) ) ) return -EFAULT;
  pchn = &pcm -> playback;
  /* fill static variables at first */
  status.rate = pchn -> real_rate;
  status.fragments = pchn -> blocks;
  status.fragment_size = pchn -> block_size;
  /* spinlock block */
  snd_spin_lock( pcm, playback, &flags );
  if ( pchn -> flags & SND_PCM_FLG_TRIGGER ) {
    ptr = pchn -> hw.pointer( pcm, pchn -> used_size );
    if ( pchn -> hw.flags & SND_PCM_HW_AUTODMA ) {
      ptr -= pchn -> tail * pchn -> block_size;
    }
    if ( ptr < 0 ) ptr = 0;
    if ( ptr > pchn -> block_size - 1 ) ptr = pchn -> block_size - 1;
  } else {
    ptr = 0;
  }
  status.count = (((pchn -> blocks_max + 1) - pchn -> used) * pchn -> block_size) - pchn -> frag_size;
  if ( status.count < 0 ) status.count = 0;
  status.queue = ((pchn -> used * pchn -> block_size) + pchn -> frag_size) - ptr;
  status.underrun = pchn -> xruns; pchn -> xruns = 0;
  status.scount = pchn -> processed_bytes + ptr;
  snd_spin_unlock( pcm, playback, &flags );
  if ( pchn -> flags & SND_PCM_FLG_TIME ) {
    /* we know about current time and about bytes in queue */
    snd_pcm_get_time( pchn, &status.time, status.queue );
    status.stime = pchn -> time;
  } else {
    status.time.tv_sec = status.time.tv_usec =
    status.stime.tv_sec = status.stime.tv_usec = 0;
  }
  copy_to_user( _status, &status, sizeof( snd_pcm_playback_status_t ) );
  return 0;
}

static int snd_pcm_record_status( snd_pcm_t *pcm, snd_pcm_record_status_t *_status )
{
  snd_pcm_record_status_t status;
  snd_pcm_channel_t *pchn;
  unsigned long flags;
  int ptr;

  if ( verify_area( VERIFY_WRITE, _status, sizeof( status ) ) ) return -EFAULT;
  pchn = &pcm -> record;
  /* fill static variables at first */
  status.rate = pchn -> real_rate;
  status.fragments = pchn -> blocks;
  status.fragment_size = pchn -> block_size;
  /* spinlock block */
  snd_spin_lock( pcm, record, &flags );
  if ( pchn -> flags & SND_PCM_FLG_TRIGGER ) {
    ptr = pchn -> hw.pointer( pcm, pchn -> used_size );
    if ( pchn -> hw.flags & SND_PCM_HW_AUTODMA ) {
      ptr -= pchn -> tail * pchn -> block_size;
    }
    if ( ptr < 0 ) ptr = 0;
    if ( ptr > pchn -> block_size - 1 ) ptr = pchn -> block_size - 1;
  } else {
    ptr = 0;
  }
  if ( !pchn -> used )
    status.count = 0;
   else
    status.count = (pchn -> used - 1) * pchn -> block_size -
                   (pchn -> block_size - pchn -> frag_size);
  status.free = ((pchn -> blocks - pchn -> used) * pchn -> block_size) - ptr;
  status.overrun = pchn -> xruns; pchn -> xruns = 0;
  status.scount = pchn -> processed_bytes + ptr;
  snd_spin_unlock( pcm, record, &flags );
  if ( pchn -> flags & SND_PCM_FLG_TIME ) {
    snd_pcm_get_time( pchn, &status.time, -(status.count+ptr) );
    status.stime = pchn -> time;
  } else {
    status.time.tv_sec = status.time.tv_usec =
    status.stime.tv_sec = status.stime.tv_usec = 0;
  }
  copy_to_user( _status, &status, sizeof( snd_pcm_playback_status_t ) );
  return 0;
}

static int snd_pcm_open_card( snd_pcm_t *pcm, struct file *file )
{
  mm_segment_t fs;
  unsigned short flags;
  int res;
  snd_pcm_format_t format;

  flags = snd_pcm_file_flags( file -> f_flags );
#ifdef SND_PCM_DEBUG_BUFFERS
  snd_printk( "snd_pcm_open (1) - pcm = 0x%lx, flags = %i, pcm->flags = 0x%x\n", (long)pcm, flags, pcm -> flags );
#endif
  if ( flags & pcm -> flags ) return -EBUSY;	/* channel(s) already used */
  if ( flags & SND_PCM_LFLG_PLAY )
    if ( !(pcm -> info_flags & SND_PCM_INFO_PLAYBACK) ) return -ENODEV;
  if ( flags & SND_PCM_LFLG_RECORD )
    if ( !(pcm -> info_flags & SND_PCM_INFO_RECORD) ) return -ENODEV;
  if ( flags == SND_PCM_LFLG_BOTH )
    if ( !(pcm -> info_flags & SND_PCM_INFO_DUPLEX) ) return -ENODEV;
  if ( flags & SND_PCM_LFLG_PLAY )
    snd_pcm_clear_channel( &pcm -> playback );
  if ( flags & SND_PCM_LFLG_RECORD )
    snd_pcm_clear_channel( &pcm -> record );
  format.format = SND_PCM_SFMT_MU_LAW;
  format.rate = SND_PCM_DEFAULT_RATE;
  format.channels = 1;
  if ( flags & SND_PCM_LFLG_PLAY )
    {
      if ( ( res = pcm -> playback.hw.open( pcm ) ) < 0 )
        return res;
      pcm -> playback.flags |= SND_PCM_FLG_ENABLE;
      pcm -> playback.ack = snd_pcm_interrupt_playback;
      fs = get_fs();
      set_fs( get_ds() );
      if ( snd_pcm_format( pcm, SND_PCM_PLAYBACK, &format ) < 0 ) {
        snd_printd( "Oops.. Playback format setup failed!!!\n" );
      }
      set_fs( fs );
      snd_pcm_init_blocks( pcm, SND_PCM_PLAYBACK );
    }
  if ( flags & SND_PCM_LFLG_RECORD )
    {
      if ( ( res = pcm -> record.hw.open( pcm ) ) < 0 )
        {
          if ( flags & SND_PCM_LFLG_PLAY )
            {
              pcm -> playback.hw.close( pcm );
              pcm -> playback.flags &= ~SND_PCM_FLG_ENABLE;
            }
          return res;
        }
      pcm -> record.flags |= SND_PCM_FLG_ENABLE;
      pcm -> record.ack = snd_pcm_interrupt_record;
      fs = get_fs();
      set_fs( get_ds() );
      if ( snd_pcm_format( pcm, SND_PCM_RECORD, &format ) < 0 ) {
        snd_printd( "Oops.. Record format setup failed!!!\n" );
      }
      set_fs( fs );
      snd_pcm_init_blocks( pcm, SND_PCM_RECORD );
    }
  pcm -> flags |= flags;

#if 0
  printk( "pcm open - done...\n" );
#endif

  return 0;
}

static void snd_pcm_close_card( snd_pcm_t *pcm, struct file *file )
{
  unsigned short flags;
  
  if ( !pcm ) return;
  flags = snd_pcm_file_flags( file -> f_flags ) & pcm -> flags;
#if 0
  snd_printk( "snd_release_pcm - flags = %i\n", flags );
#endif
  if ( flags & SND_PCM_LFLG_PLAY )
    {
      snd_pcm_flush_playback( pcm );		/* synchronize playback */
      pcm -> playback.hw.trigger( pcm, 0 );	/* for sure */
      pcm -> playback.hw.close( pcm );
      pcm -> flags &= ~SND_PCM_LFLG_PLAY;
    }
  if ( flags & SND_PCM_LFLG_RECORD )
    {
      pcm -> playback.flags |= SND_PCM_FLG_ABORT;
      snd_pcm_drain_record( pcm );		/* and record, data can't be read, so flush isn't needed */
      pcm -> record.hw.trigger( pcm, 0 );	/* for sure */
      pcm -> record.hw.close( pcm );
      pcm -> flags &= ~SND_PCM_LFLG_RECORD;
    }
#if 0
  printk( "release pcm: done\n" );
#endif
}

static int snd_pcm_open( unsigned short minor, int cardnum, int device, struct file *file )
{
  int res;
  snd_pcm_t *pcm;

  pcm = snd_pcm_devices[ (cardnum * SND_PCM_DEVICES) + device ];
  if ( !pcm ) return -ENODEV;
  snd_mutex_down( pcm, open );
  if ( ( res = snd_pcm_open_card( pcm, file ) ) < 0 ) {
    snd_mutex_up( pcm, open );
    return res;
  }
  file -> private_data = pcm;
  MOD_INC_USE_COUNT;
  pcm -> card -> use_inc( pcm -> card );
  snd_mutex_up( pcm, open );
  return 0;
}

static int snd_pcm_release( unsigned short minor, int cardnum, int device, struct file *file )
{
  snd_pcm_t *pcm;
 
  if ( file -> private_data ) {
    pcm = (snd_pcm_t *)file -> private_data;
    snd_mutex_down( pcm, open );
    snd_pcm_close_card( pcm, file );
    snd_mutex_up( pcm, open );
    pcm -> card -> use_dec( pcm -> card );
  }
  MOD_DEC_USE_COUNT;
  return 0;
}

static int snd_pcm_ioctl( struct file *file, unsigned int cmd, unsigned long arg )
{
  snd_pcm_t *pcm;
  snd_card_t *card;
  unsigned short flags;
  int tmp;

  pcm = (snd_pcm_t *)file -> private_data;
  card = pcm -> card;
  if ( ( ( cmd >> 8 ) & 0xff ) != 'A' ) return -EIO;
  flags = snd_pcm_file_flags( file -> f_flags );
#if 0
  printk( "cmd = 0x%x, arg = 0x%x, flags = 0x%x\n", cmd, snd_ioctl_in( (long *)arg ), flags );
#endif
  switch ( cmd ) {
    case SND_PCM_IOCTL_PVERSION:
      return snd_ioctl_out( (long *)arg, SND_PCM_VERSION );
    case SND_PCM_IOCTL_INFO:
      return snd_pcm_info( pcm, (snd_pcm_info_t *)arg );
    case SND_PCM_IOCTL_PLAYBACK_INFO:
      if ( !(flags & SND_PCM_LFLG_PLAY) ) return -EIO;
      return snd_pcm_playback_info( pcm, (snd_pcm_playback_info_t *)arg );
    case SND_PCM_IOCTL_RECORD_INFO:
      if ( !(flags & SND_PCM_LFLG_RECORD) ) return -EIO;
      return snd_pcm_record_info( pcm, (snd_pcm_record_info_t *)arg );
    case SND_PCM_IOCTL_PLAYBACK_FORMAT:
      if ( !(flags & SND_PCM_LFLG_PLAY) ) return -EIO;
      return snd_pcm_format( pcm, SND_PCM_PLAYBACK, (snd_pcm_format_t *)arg );
    case SND_PCM_IOCTL_RECORD_FORMAT:
      if ( !(flags & SND_PCM_LFLG_RECORD) ) return -EIO;
      return snd_pcm_format( pcm, SND_PCM_RECORD, (snd_pcm_format_t *)arg );
    case SND_PCM_IOCTL_PLAYBACK_PARAMS:
      if ( !(flags & SND_PCM_LFLG_PLAY) ) return -EIO;
      return snd_pcm_playback_params( pcm, (snd_pcm_playback_params_t *)arg );
    case SND_PCM_IOCTL_RECORD_PARAMS:
      if ( !(flags & SND_PCM_LFLG_RECORD) ) return -EIO;
      return snd_pcm_record_params( pcm, (snd_pcm_record_params_t *)arg );
    case SND_PCM_IOCTL_PLAYBACK_STATUS:
      if ( !(flags & SND_PCM_LFLG_PLAY) ) return -EIO;
      return snd_pcm_playback_status( pcm, (snd_pcm_playback_status_t *)arg );
    case SND_PCM_IOCTL_RECORD_STATUS:
      if ( !(flags & SND_PCM_LFLG_RECORD) ) return -EIO;
      return snd_pcm_record_status( pcm, (snd_pcm_record_status_t *)arg );
    case SND_PCM_IOCTL_DRAIN_PLAYBACK:
      if ( !(flags & SND_PCM_LFLG_PLAY) ) return -EIO;
      return snd_pcm_drain_playback( pcm );
    case SND_PCM_IOCTL_FLUSH_PLAYBACK:
      if ( !(flags & SND_PCM_LFLG_PLAY) ) return -EIO;
      return snd_pcm_flush_playback( pcm );
    case SND_PCM_IOCTL_FLUSH_RECORD:
      if ( !(flags & SND_PCM_LFLG_RECORD) ) return -EIO;
      return snd_pcm_flush_record( pcm );
    case SND_PCM_IOCTL_PLAYBACK_TIME:
      if ( !(flags & SND_PCM_LFLG_PLAY) ) return -EIO;
      tmp = snd_ioctl_in( (long *)arg );
      if ( tmp )
        pcm -> playback.flags |= SND_PCM_FLG_TIME;
       else
        pcm -> playback.flags &= ~SND_PCM_FLG_TIME;
      snd_ioctl_out( (long *)arg, tmp != 0 );
      return 0;
    case SND_PCM_IOCTL_RECORD_TIME:
      if ( !(flags & SND_PCM_LFLG_RECORD) ) return -EIO;
      tmp = snd_ioctl_in( (long *)arg );
      if ( tmp )
        pcm -> record.flags |= SND_PCM_FLG_TIME;
       else
        pcm -> record.flags &= ~SND_PCM_FLG_TIME;
      snd_ioctl_out( (long *)arg, tmp != 0 );
      return 0;
#ifdef SNDCFG_DEBUG
    default:
      snd_printk( "pcm: unknown command = 0x%x\n", cmd ); 
#endif
  }
  return -EIO;
}

static int snd_pcm_control_ioctl( snd_card_t *card, snd_control_t *control, unsigned int cmd, unsigned long arg )
{
  unsigned int tmp;
  int idx;

  tmp = card -> number << 2;
  switch ( cmd ) {
    case SND_CTL_IOCTL_HW_INFO:
      {
        struct snd_ctl_hw_info *ptr = (struct snd_ctl_hw_info *)arg;
        
        ptr -> pcmdevs = 0;
        for ( idx = SND_PCM_DEVICES - 1; idx >= 0; idx-- ) {
          if ( snd_pcm_devices[ tmp + idx ] ) {
             ptr -> pcmdevs = idx + 1;
             break;
          }
        }
        return 0;
      }
    case SND_CTL_IOCTL_PCM_DEVICE:
      {
        int val = snd_ioctl_in( (long *)arg );
        if ( val < 0 || val >= SND_PCM_DEVICES ) return -EINVAL;
        if ( !snd_pcm_devices[ tmp + val ] ) return -EINVAL;
        control -> pcm_device = val;
        return 0;
      }
    case SND_CTL_IOCTL_PCM_INFO:
      return snd_pcm_info( snd_pcm_devices[ tmp + control -> pcm_device ], (snd_pcm_info_t *)arg );
    case SND_CTL_IOCTL_PCM_PLAYBACK_INFO:
      return snd_pcm_playback_info( snd_pcm_devices[ tmp + control -> pcm_device ], (snd_pcm_playback_info_t *)arg );
    case SND_CTL_IOCTL_PCM_RECORD_INFO:
      return snd_pcm_record_info( snd_pcm_devices[ tmp + control -> pcm_device ], (snd_pcm_record_info_t *)arg );
  }
  return -EAGAIN;
}

static long snd_pcm_read( struct file *file, char *buf, long count )
{
  snd_pcm_t *pcm;

  pcm = (snd_pcm_t *)file -> private_data;
  if ( !pcm ) return -EIO;
  if ( !( snd_pcm_file_flags( file -> f_flags ) & SND_PCM_LFLG_RECORD ) ||
       !( pcm -> flags & SND_PCM_LFLG_RECORD ) ) return -EIO;
  return snd_pcm_buffer_to_user( file, pcm, buf, count );
}

static long snd_pcm_write( struct file *file, const char *buf, long count )
{
  snd_pcm_t *pcm;

  pcm = (snd_pcm_t *)file -> private_data;
  if ( !pcm ) return -EIO;
  if ( !( snd_pcm_file_flags( file -> f_flags ) & SND_PCM_LFLG_PLAY ) ||
       !( pcm -> flags & SND_PCM_LFLG_PLAY ) ) return -EIO;
  return snd_pcm_user_to_buffer( file, pcm, buf, count );
}

#ifdef SND_POLL
static unsigned int snd_pcm_poll( struct file *file, poll_table *wait )
{
  snd_pcm_t *pcm;
  unsigned long flags;
  unsigned int mask;
  snd_pcm_channel_t *record, *playback;
  unsigned short fflags;

  fflags = snd_pcm_file_flags( file -> f_flags );
  pcm = (snd_pcm_t *)file -> private_data;
  if ( !pcm ) return 0;

  record = &pcm -> record;
  playback = &pcm -> playback;
  
  if ( fflags & SND_PCM_LFLG_RECORD ) {
    snd_pcm_trigger_record( pcm, record );
    snd_spin_lock( pcm, record_sleep, &flags );
    record -> flags |= SND_PCM_FLG_SLEEP;
    snd_poll_wait( file, pcm, record, wait );
    snd_spin_unlock( pcm, record_sleep, &flags );
  }
  if ( fflags & SND_PCM_LFLG_PLAY ) {
    snd_spin_lock( pcm, playback_sleep, &flags );
    playback -> flags |= SND_PCM_FLG_SLEEP;
    snd_poll_wait( file, pcm, playback, wait );
    snd_spin_unlock( pcm, playback_sleep, &flags );
  }

  mask = 0;
  if ( fflags & SND_PCM_LFLG_RECORD ) {
    if ( snd_pcm_record_ok( pcm ) ) mask |= POLLIN | POLLRDNORM;
  }
  if ( fflags & SND_PCM_LFLG_PLAY ) {
    if ( snd_pcm_playback_ok( pcm ) ) mask |= POLLOUT | POLLWRNORM;
  }

  return mask;
}
#else
static int snd_pcm_select( struct file *file, int sel_type, select_table *wait )
{
  snd_pcm_t *pcm;
  unsigned long flags;
  snd_pcm_channel_t *pchn;
  unsigned short fflags;

  fflags = snd_pcm_file_flags( file -> f_flags );
  pcm = (snd_pcm_t *)file -> private_data;
  if ( !pcm ) return -EIO;
  switch ( sel_type ) {
    case SEL_IN:
      if ( !(fflags & SND_PCM_LFLG_RECORD) ) return 0;
      pchn = &pcm -> record;
      snd_pcm_trigger_record( pcm, pchn );
      snd_spin_lock( pcm, record_sleep, &flags );
      if ( !snd_pcm_record_ok( pcm ) )
        {
          pchn -> flags |= SND_PCM_FLG_SLEEP;
          snd_select_wait( pcm, record, wait );
          snd_spin_unlock( pcm, record_sleep, &flags );
          return 0;
        }
      pchn -> flags &= ~SND_PCM_FLG_SLEEP;
      snd_spin_unlock( pcm, record_sleep, &flags );
      return 1;
    case SEL_OUT:
      if ( !(fflags & SND_PCM_LFLG_PLAY) ) return 0;
      pchn = &pcm -> playback;
      snd_spin_lock( pcm, playback_sleep, &flags );
      if ( !snd_pcm_playback_ok( pcm ) )
        {
          pchn -> flags |= SND_PCM_FLG_SLEEP;
          snd_select_wait( pcm, playback, wait );
          snd_spin_unlock( pcm, playback_sleep, &flags );
          return 0;
        }
      pchn -> flags &= ~SND_PCM_FLG_SLEEP;
      snd_spin_unlock( pcm, playback_sleep, &flags );
      return 1;
    case SEL_EX:
      break;
  }
  return 0;
}
#endif

/*
 *  Register functions
 */

static snd_minor_t snd_pcm_reg = {
  "digital audio",
  
  NULL,				/* unregister */
  
  NULL,				/* lseek */
  snd_pcm_read,			/* read */
  snd_pcm_write,		/* write */
  snd_pcm_open,			/* open */
  snd_pcm_release,		/* release */
#ifdef SND_POLL
  snd_pcm_poll,			/* poll */
#else
  snd_pcm_select,		/* select */
#endif
  snd_pcm_ioctl,		/* ioctl */
  NULL,				/* mmap */
};

snd_pcm_t *snd_pcm_new_device( snd_card_t *card, char *id )
{
  snd_pcm_t *pcm;

  if ( !card ) return NULL;
  pcm = (snd_pcm_t *)snd_malloc( sizeof( snd_pcm_t ) );
  if ( !pcm ) return NULL;
  memset( pcm, 0, sizeof( snd_pcm_t ) );
  pcm -> flags = SND_PCM_LFLG_NONE;
  pcm -> card = card;
  snd_sleep_prepare( pcm, playback );
  snd_sleep_prepare( pcm, record );
  snd_mutex_prepare( pcm, open );
  snd_spin_prepare( pcm, playback );
  snd_spin_prepare( pcm, record );
  snd_spin_prepare( pcm, playback_sleep );
  snd_spin_prepare( pcm, record_sleep );
  if ( id ) {
    strncpy( pcm -> id, id, sizeof( pcm -> id ) - 1 );
  }
  return pcm;
}

int snd_pcm_free( snd_pcm_t *pcm )
{
  if ( !pcm ) return -EINVAL;
  if ( pcm -> playback.hw.private_free )
    pcm -> playback.hw.private_free( pcm -> playback.hw.private_data );
  if ( pcm -> record.hw.private_free )
    pcm -> record.hw.private_free( pcm -> record.hw.private_data );
  if ( pcm -> private_free )
    pcm -> private_free( pcm -> private_data );
  snd_free( pcm, sizeof( snd_pcm_t ) );
  return 0;  
}

int snd_pcm_register( snd_pcm_t *pcm, int pcm_device )
{
  int idx, err;
  unsigned short minor;

  snd_mutex_down_static( register );
  if ( pcm_device < 0 || pcm_device >= SND_PCM_DEVICES ) {
    snd_mutex_up_static( register );
    return -EINVAL;
  }
  idx = (pcm -> card -> number * SND_PCM_DEVICES) + pcm_device;
  if ( snd_pcm_devices[ idx ] ) {
    snd_mutex_up_static( register );
    return -EBUSY;
  }
  pcm -> device = pcm_device;
  snd_pcm_devices[ idx ] = pcm;
  minor = SND_MINOR_PCM + idx;
  if ( (err = snd_register_minor( minor, &snd_pcm_reg )) < 0 ) {
    snd_pcm_devices[ idx ] = NULL;
    snd_mutex_up_static( register );
    return err;
  }
  if ( snd_pcm_oss_register )
    snd_pcm_oss_register( minor );
  snd_pcm_proc_init( pcm );
  snd_mutex_up_static( register );
  return 0;
}

int snd_pcm_unregister( snd_pcm_t *pcm )
{
  int idx;
  
  if ( !pcm ) return -EINVAL;
  snd_mutex_down_static( register );  
  idx = (pcm -> card -> number * SND_PCM_DEVICES) + pcm -> device;
  if ( snd_pcm_devices[ idx ] != pcm ) {
    snd_mutex_up_static( register );
    return -EINVAL;
  }
  snd_pcm_proc_done( pcm );
  snd_unregister_minor( SND_MINOR_PCM + idx );
  if ( snd_pcm_oss_unregister ) {
    snd_pcm_oss_unregister( SND_MINOR_PCM + idx );
  }
  snd_pcm_devices[ idx ] = NULL;
  snd_mutex_up_static( register );
  return snd_pcm_free( pcm );
}

/*
 *  ENTRY functions
 */

#ifndef LINUX_2_1
extern struct symbol_table snd_symbol_table_pcmexport;
#endif

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

void cleanup_module( void )
{
  snd_control_unregister_ioctl( snd_pcm_control_ioctl );
}
