/*
 *  Abstract routines for MIXER control
 *  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_OSS_COMPAT__
#define SND_MAIN_OBJECT_FILE
#include "driver.h"
#include "minors.h"
#include "mixer.h"
#include "info.h"
#include "control.h"

#define SND_OSS_VERSION		((3<<16)|(8<<8)|(1<<4)|(0))	/* 3.8.1a */

#define SND_MIXERS		(SND_CARDS * SND_MINOR_MIXERS)

static snd_kmixer_t *snd_mixers[ SND_MIXERS ] = { [ 0 ... (SND_MIXERS-1) ] = NULL };

snd_mutex_define_static( register );

snd_kmixer_channel_t *snd_mixer_oss_channel( snd_kmixer_t *mixer, int device )
{
  int idx;
  snd_kmixer_channel_t *channel;
  
  for ( idx = 0; idx < mixer -> channels_count; idx++ ) {
    channel = mixer -> channels[ idx ];
    if ( channel -> hw.ossdev == device )
      return channel;
  }
  return NULL;
}

static int snd_mixer_open( unsigned short minor, int cardnum, int device, struct file *file )
{
  snd_kmixer_file_t *mfile, *mfile1;
  snd_kmixer_t *mixer;

  if ( !(mixer = snd_mixers[ (cardnum * SND_MINOR_MIXERS) + device ]) ) return -ENODEV;
  mfile = (snd_kmixer_file_t *)snd_malloc( sizeof( snd_kmixer_file_t ) );
  if ( !mfile ) return -ENOMEM;
  memset( mfile, 0, sizeof( snd_kmixer_file_t ) );
  snd_sleep_prepare( mfile, change );
  mfile -> mixer = mixer;
  if ( !mfile -> mixer ) {
    snd_free( mfile, sizeof( snd_kmixer_file_t ) );
    return -ENOMEM;
  }
  file -> private_data = mfile;
  MOD_INC_USE_COUNT;
  mixer -> card -> use_inc( mixer -> card );
  snd_mutex_down( mixer, ffile );
  if ( mixer -> ffile ) {
    for ( mfile1 = mixer -> ffile; mfile1 -> next; mfile1 = mfile1 -> next );
    mfile1 -> next = mfile;
  } else {
    mixer -> ffile = mfile;
  }
  snd_mutex_up( mixer, ffile );
  return 0;
}

static int snd_mixer_release( unsigned short minor, int cardnum, int device, struct file *file )
{
  snd_kmixer_file_t *mfile, *mfile1;
  snd_kmixer_t *mixer;

  if ( file -> private_data ) {
    mfile = (snd_kmixer_file_t *)file -> private_data;
    mixer = mfile -> mixer;
    snd_mutex_down( mixer, ffile );
    if ( mixer -> ffile == mfile ) {
      mixer -> ffile = mfile -> next;
    } else {
      for ( mfile1 = mixer -> ffile; mfile1 -> next != mfile; mfile1 = mfile1 -> next );
      mfile1 -> next = mfile -> next;
    }
    snd_mutex_up( mixer, ffile );
    snd_free( file -> private_data, sizeof( snd_kmixer_file_t ) );
    file -> private_data = NULL;
    mixer -> card -> use_dec( mixer -> card );
  }
  MOD_DEC_USE_COUNT;
  return 0;
}

void snd_mixer_set_kernel_mute( snd_kmixer_t *mixer, unsigned int priority, unsigned short mute ) {
  int idx, mask, left, right;
  snd_kmixer_channel_t *channel;

  for ( idx = 0; idx < mixer -> channels_count; idx++ ) {
    channel = mixer -> channels[ idx ];
    if ( channel -> hw.priority == priority ) {
      if ( !channel -> hw.stereo )
        mute = mute & SND_MIX_MUTE ? SND_MIX_MUTE : 0;
      if ( channel -> kmute != mute ) {
        mask = channel -> kmute = mute;
        mask |= channel -> umute;
        if ( mask != channel -> mute ) { 
          channel -> mute = mask;
          if ( channel -> hw.mute && channel -> hw.set_mute ) {
            channel -> hw.set_mute( mixer, channel, mask );
          } else {
            left = mask & SND_MIX_MUTE_LEFT ? channel -> hw.min : channel -> uleft;
            right = mask & SND_MIX_MUTE_RIGHT ? channel -> hw.min : channel -> uright;
            if ( channel -> left != left || channel -> right != right ) {
              if ( channel -> hw.set_volume_level )
                channel -> hw.set_volume_level( mixer, channel, left, right );
              channel -> left = left;
              channel -> right = right;
            }        
          }
        }
      }
    }
  }
}

static void snd_mixer_notify_change( snd_kmixer_file_t *mfile, unsigned int channel )
{
  unsigned long flags;
  snd_kmixer_file_t *mfile1;
  snd_kmixer_t *mixer;
  
  mixer = mfile -> mixer;
#if 0
  printk( "notify change: 0x%x (%i)\n", (int)(1 << channel), (int)channel );
#endif
  channel = 1 << channel;
  for ( mfile1 = mixer -> ffile; mfile1; mfile1 = mfile1 -> next ) {
    if ( mfile1 != mfile ) {
      snd_spin_lock( mixer, lock, &flags );
      if ( mfile1 -> changes & channel ) {
        snd_spin_unlock( mixer, lock, &flags );
        continue;
      }
      mfile1 -> changes |= channel;
      if ( snd_getlock( mfile1, change ) & SND_WK_SLEEP ) {
        snd_getlock( mfile1, change ) &= ~SND_WK_SLEEP;
        snd_wakeup( mfile1, change );
      }
      snd_spin_unlock( mixer, lock, &flags );
    }
  }
}

static int snd_mixer_device_read( snd_kmixer_file_t *mfile, snd_mixer_channel_t *_device )
{
  unsigned long flags;
  snd_kmixer_t *mixer;
  snd_mixer_channel_t device;
  snd_kmixer_channel_t *channel;
  int tmp;

  mixer = mfile -> mixer;
  if ( verify_area( VERIFY_READ, _device, sizeof( device ) ) ) return -EFAULT;
  if ( verify_area( VERIFY_WRITE, _device, sizeof( device ) ) ) return -EFAULT;
  copy_from_user( &device, _device, sizeof( device ) );
  tmp = device.channel;
  memset( &device, 0, sizeof( snd_mixer_channel_t ) );
  device.channel = tmp;
  if ( tmp >= mixer -> channels_count ) return -ENODEV;
  channel = mixer -> channels[ tmp ];
  snd_spin_lock( mixer, lock, &flags );
  device.flags = 0;
  if ( channel -> record )
    device.flags |= SND_MIXER_FLG_RECORD;
  if ( channel -> umute & SND_MIX_MUTE_LEFT )
    device.flags |= SND_MIXER_FLG_MUTE_LEFT;
  if ( channel -> umute & SND_MIX_MUTE_RIGHT )
    device.flags |= SND_MIXER_FLG_MUTE_RIGHT;
  if ( mfile -> exact ) {
    device.left = channel -> uleft;
    device.right = channel -> uright;
  } else {
    device.left = channel -> aleft;
    device.right = channel -> aright;
  }
  if ( channel -> hw.compute_dB ) {
    device.left_dB = channel -> hw.compute_dB( mixer, channel, channel -> uleft );
    device.right_dB = channel -> hw.compute_dB( mixer, channel, channel -> uright );
  } else {
    tmp = channel -> hw.max - channel -> hw.min;
    device.left_dB = ((((channel -> hw.max_dB - channel -> hw.min_dB) * (channel -> uleft - channel -> hw.min)) + (tmp >> 1)) / tmp) + channel -> hw.min_dB;
    device.left_dB -= device.left_dB % channel -> hw.step_dB;
    device.right_dB = ((((channel -> hw.max_dB - channel -> hw.min_dB) * (channel -> uright - channel -> hw.min)) + (tmp >> 1)) / tmp) + channel -> hw.min_dB;
    device.right_dB -= device.right_dB % channel -> hw.step_dB;
  }
  snd_spin_unlock( mixer, lock, &flags );
  copy_to_user( _device, &device, sizeof( device ) );
  return 0;
}

static int snd_mixer_device_write( snd_kmixer_file_t *mfile, snd_mixer_channel_t *_device )
{
  unsigned int mask, left, right;
  snd_mixer_channel_t device;
  snd_kmixer_t *mixer;
  snd_kmixer_channel_t *channel;
  int force = 0, change = 0, tmp, aleft, aright;

  mixer = mfile -> mixer;
  if ( verify_area( VERIFY_READ, _device, sizeof( device ) ) ) return -EFAULT;
  copy_from_user( &device, _device, sizeof( device ) );
  if ( device.channel >= mixer -> channels_count ) return -ENODEV;
  channel = mixer -> channels[ device.channel ];

  force = (device.flags & SND_MIXER_FLG_FORCE) != 0;

  /* record source */

  mask = 0;
  if ( (device.flags & SND_MIXER_FLG_RECORD) && (channel -> hw.record) )
    mask = 1;
  if ( force || channel -> record != mask ) {
    if ( channel -> hw.set_record_source )
      channel -> hw.set_record_source( mixer, channel, mask );
    channel -> record = mask;
    change = 1;
  }

  /* mute */

  mask = 0;
  if ( !force || channel -> hw.mute ) {
    if ( channel -> hw.stereo ) {
      if ( device.flags & SND_MIXER_FLG_MUTE_LEFT ) mask |= SND_MIX_MUTE_LEFT;
      if ( device.flags & SND_MIXER_FLG_MUTE_RIGHT ) mask |= SND_MIX_MUTE_RIGHT;
    } else {
      if ( device.flags & SND_MIXER_FLG_MUTE ) mask = SND_MIX_MUTE;
    }
  }
  if ( channel -> umute != mask )
    change = 1;
  channel -> umute = mask;

  /* volume */

  if ( !(device.flags & SND_MIXER_FLG_DECIBEL) ) {
    left = device.left;
    right = device.right;
  } else {
    if ( device.left_dB < channel -> hw.min_dB ) device.left_dB = channel -> hw.min_dB;
    if ( device.right_dB < channel -> hw.min_dB ) device.right_dB = channel -> hw.min_dB;
    if ( device.left_dB > channel -> hw.max_dB ) device.left_dB = channel -> hw.max_dB;
    if ( device.right_dB > channel -> hw.max_dB ) device.right_dB = channel -> hw.max_dB;
    if ( channel -> hw.compute_linear ) {
      left = channel -> hw.compute_linear( mixer, channel, device.left_dB );
      right = channel -> hw.compute_linear( mixer, channel, device.right_dB );
    } else {
      tmp = channel -> hw.max_dB - channel -> hw.min_dB;
      device.left_dB %= channel -> hw.step_dB;
      device.right_dB %= channel -> hw.step_dB;
      left = ((((channel -> hw.max - channel -> hw.min) * (device.left_dB - channel -> hw.min_dB)) + (tmp >> 1)) / tmp) + channel -> hw.min;
      right = ((((channel -> hw.max - channel -> hw.min) * (device.right_dB - channel -> hw.min_dB)) + (tmp >> 1)) / tmp) + channel -> hw.min;
    }
  }  
  if ( mfile -> exact || (device.flags & SND_MIXER_FLG_DECIBEL) ) {
    if ( left < channel -> hw.min ) left = channel -> hw.min;
    if ( right < channel -> hw.min ) right = channel -> hw.min;
    if ( left > channel -> hw.max ) left = channel -> hw.max;
    if ( right > channel -> hw.max ) right = channel -> hw.max;
    tmp = channel -> hw.max - channel -> hw.min;
    aleft = ( ( 100 * ( left - channel -> hw.min ) ) + ( tmp >> 1 ) ) / tmp;
    aright = ( ( 100 * ( right - channel -> hw.min ) ) + ( tmp >> 1 ) ) / tmp;
  } else {
    if ( left < 0 ) left = 0;
    if ( right < 0 ) right = 0;
    if ( left > 100 ) left = 100;
    if ( right > 100 ) right = 100;
    left = ( ( channel -> hw.max * (aleft = left) ) + 50 ) / 100;
    right = ( ( channel -> hw.max * (aright = right) ) + 50 ) / 100;
  }
  if ( channel -> aleft != aleft || channel -> aright != aright )
    change = 1;
  channel -> aleft = aleft;
  channel -> aright = aright;
  if ( channel -> uleft != left || channel -> uright != right )
    change = 1;
  channel -> uleft = left;
  channel -> uright = right;

  /* make changes */

  left = channel -> uleft;
  right = channel -> uright;
  mask = channel -> umute | channel -> kmute;
  if ( force || channel -> mute != mask ) {
    channel -> mute = mask;
    if ( channel -> hw.mute && channel -> hw.set_mute )
      channel -> hw.set_mute( mixer, channel, mask );
    change = 1;
  }
  if ( !channel -> hw.mute || !channel -> hw.set_mute ) {
    if ( channel -> mute & SND_MIX_MUTE_LEFT ) left = channel -> hw.min;
    if ( channel -> mute & SND_MIX_MUTE_RIGHT ) right = channel -> hw.min;
  }
#if 0
  snd_printk( "hw.mute = 0x%x, mute = 0x%x, left = %i, right = %i\n", channel -> hw.mute, channel -> mute, left, right );
#endif
  if ( force || channel -> left != left || channel -> right != right ) {
    channel -> left = left;
    channel -> right = right;
    if ( channel -> hw.set_volume_level )
      channel -> hw.set_volume_level( mixer, channel, left, right );
    change = 1;
  }

  /* let other mixers know that something were changed */

  if ( change )
    snd_mixer_notify_change( mfile, channel -> channel );
  
  return 0;
}

static int snd_mixer_special_read( snd_kmixer_t *mixer, struct snd_mixer_special *_special )
{
  int err = 0;
  struct snd_mixer_special special;
  
  if ( verify_area( VERIFY_READ, _special, sizeof( special ) ) ) return -EFAULT;
  copy_from_user( &special, _special, sizeof( special ) );
  if ( special.what == SND_MIXER_S_NONE )
    return 0;		/* can be used for feature testing */
  if ( mixer -> hw.get_special )
    err = mixer -> hw.get_special( mixer, &special );
   else
    err = -ENXIO;
  if ( !err ) {
    if ( verify_area( VERIFY_WRITE, _special, sizeof( special ) ) ) return -EFAULT;
    copy_to_user( _special, &special, sizeof( special ) );
  }
  return err;
}

static int snd_mixer_special_write( snd_kmixer_t *mixer, struct snd_mixer_special *_special )
{
  int err = 0;
  struct snd_mixer_special special;
  
  if ( verify_area( VERIFY_READ, _special, sizeof( special ) ) ) return -EFAULT;
  copy_from_user( &special, _special, sizeof( special ) );
  if ( special.what == SND_MIXER_S_NONE )
    return 0;		/* can be used for feature testing */
  if ( mixer -> hw.get_special )
    err = mixer -> hw.get_special( mixer, &special );
   else
    err = -ENXIO;
  return err;
}

static int snd_mixer_info( snd_kmixer_t *mixer, snd_mixer_info_t *_info )
{
  snd_mixer_info_t info;
  
  if ( verify_area( VERIFY_WRITE, _info, sizeof( info ) ) ) return -EFAULT;
  memset( &info, 0, sizeof( snd_mixer_info_t ) );
  info.type = mixer -> card -> type;
  info.channels = mixer -> channels_count;
  info.caps = mixer -> hw.caps;
  strncpy( info.id, mixer -> id, sizeof( info.id ) );
  strncpy( info.name, mixer -> name, sizeof( info.name ) );
  copy_to_user( _info, &info, sizeof( info ) );
  return 0;
}

static int snd_mixer_device_info( snd_kmixer_file_t *mfile, snd_mixer_channel_info_t *_info )
{
  snd_kmixer_t *mixer;
  snd_mixer_channel_info_t info;
  snd_kmixer_channel_t *channel;
  int channel_idx, i;

  mixer = mfile -> mixer;
  if ( verify_area( VERIFY_READ, _info, sizeof( info ) ) ) return -EFAULT;
  if ( verify_area( VERIFY_WRITE, _info, sizeof( info ) ) ) return -EFAULT;
  copy_from_user( &info, _info, sizeof( info ) );
  channel_idx = info.channel;
  memset( &info, 0, sizeof( snd_mixer_channel_info_t ) );
  info.channel = channel_idx;
  if ( channel_idx >= mixer -> channels_count ) return -ENODEV;
  channel = mixer -> channels[ channel_idx ];  
  strncpy( info.name, channel -> hw.name, sizeof( info.name ) - 1 );
  if ( channel -> hw.parent_priority != SND_MIXER_PRI_PARENT ) {
    for ( i = 0; i < mixer -> channels_count; i++ ) {
      if ( mixer -> channels[ i ] -> hw.priority == channel -> hw.parent_priority ) {
        info.parent = i;
        break;
      }
    }
    if ( i >= mixer -> channels_count ) {
      snd_printd( "Oops... Parent 0x%x not found!!!\n", channel -> hw.parent_priority );
      info.parent = SND_MIXER_PRI_PARENT;
    }
  } else {
    info.parent = SND_MIXER_PRI_PARENT;
  }
  info.caps = 0;
  if ( channel -> hw.record )
    info.caps |= SND_MIXER_CINFO_CAP_RECORD;
  if ( channel -> hw.stereo )
    info.caps |= SND_MIXER_CINFO_CAP_STEREO;
  info.caps |= SND_MIXER_CINFO_CAP_MUTE;	/* mute is always emulated */
  if ( channel -> hw.mute )
    info.caps |= SND_MIXER_CINFO_CAP_HWMUTE;
  if ( channel -> hw.digital )
    info.caps |= SND_MIXER_CINFO_CAP_DIGITAL;
  if ( channel -> hw.input )
    info.caps |= SND_MIXER_CINFO_CAP_INPUT;
  if ( mfile -> exact ) {
    info.min = channel -> hw.min;
    info.max = channel -> hw.max;
  } else {
    info.min = 0;
    info.max = 100;
  }
  info.min_dB = channel -> hw.min_dB;
  info.max_dB = channel -> hw.max_dB;
  info.step_dB = channel -> hw.step_dB;
  copy_to_user( _info, &info, sizeof( info ) );
  return 0;
}

static int snd_mixer_set_recsrc( snd_kmixer_file_t *mfile, int recsrc )
{
  int idx, value;
  snd_kmixer_t *mixer;
  snd_kmixer_channel_t *channel;

  mixer = mfile -> mixer;
  for ( idx = 0; idx < SND_MIXER_OSS_DEVS; idx++ ) {
    channel = snd_mixer_oss_channel( mixer, idx );
    if ( channel == NULL ) continue;
    value = (recsrc & (1 << idx)) ? 1 : 0;
    if ( channel -> record != value ) {
      channel -> record = value;
      if ( channel -> hw.record && channel -> hw.set_record_source )
        channel -> hw.set_record_source( mixer, channel, value );
      snd_mixer_notify_change( mfile, channel -> channel );
    }
  }
  return 0;
}

static int snd_mixer_devmask( snd_kmixer_t *mixer )
{
  int result, idx, ossdev;
  
  result = 0;
  for ( idx = 0; idx < mixer -> channels_count; idx++ ) {
    ossdev = mixer -> channels[ idx ] -> hw.ossdev;
    if ( ossdev == SND_MIXER_OSS_UNKNOWN ) continue;
    result |= (1 << ossdev);
  }
  return result;
}

static int snd_mixer_stereodevs( snd_kmixer_t *mixer )
{
  int result, idx;
  snd_kmixer_channel_t *channel;
  
  result = 0;
  for ( idx = 0; idx < mixer -> channels_count; idx++ ) {
    channel = mixer -> channels[ idx ];
    if ( channel -> hw.ossdev == SND_MIXER_OSS_UNKNOWN ) continue;
    if ( channel -> hw.stereo )
      result |= (1 << channel -> hw.ossdev);
  }
  return result;
}

static int snd_mixer_recmask( snd_kmixer_t *mixer )
{
  int result, idx;
  snd_kmixer_channel_t *channel;
  
  result = 0;
  for ( idx = 0; idx < mixer -> channels_count; idx++ ) {
    channel = mixer -> channels[ idx ];
    if ( channel -> hw.ossdev == SND_MIXER_OSS_UNKNOWN ) continue;
    if ( channel -> hw.record )
      result |= (1 << channel -> hw.ossdev);
  }
  return result;
}

static int snd_mixer_get_recsrc( snd_kmixer_t *mixer )
{
  int result, idx;
  snd_kmixer_channel_t *channel;
  
  result = 0;
  for ( idx = 0; idx < mixer -> channels_count; idx++ ) {
    channel = mixer -> channels[ idx ];
    if ( channel -> hw.ossdev == SND_MIXER_OSS_UNKNOWN ) continue;
    if ( channel -> record )
      result |= (1 << channel -> hw.ossdev);
  }
  return result;
}

static int snd_mixer_osscaps( snd_kmixer_t *mixer )
{
  int result;

  result = 0;
  if ( mixer -> hw.caps & SND_MIXER_INFO_CAP_EXCL_RECORD )
    result |= SND_MIXER_OSS_CAP_EXCL_INPUT;
  return result;
}

static int snd_mixer_set_volume( snd_kmixer_file_t *mfile, int oss_device, int volume )
{
  mm_segment_t fs;
  int err;
  snd_kmixer_t *mixer;
  snd_kmixer_channel_t *channel;
  snd_mixer_channel_t device;

  mixer = mfile -> mixer;
  channel = snd_mixer_oss_channel( mixer, oss_device );
  if ( !channel ) return -ENXIO;
  device.channel = channel -> channel;
  fs = get_fs();
  set_fs( get_ds() );
  if ( (err = snd_mixer_device_read( mfile, &device )) < 0 ) {
    set_fs( fs );
    return err;
  }
  device.left = volume & 0xff;
  device.right = (volume >> 8) & 0xff;
  if ( (err = snd_mixer_device_write( mfile, &device )) < 0 ) {
    set_fs( fs );
    return err;
  }
  set_fs( fs );
  return (unsigned short)device.left |
         ((unsigned short)device.right << 8);
}

static int snd_mixer_get_volume( snd_kmixer_file_t *mfile, int oss_device )
{
  mm_segment_t fs;
  int err;
  snd_kmixer_t *mixer;
  snd_kmixer_channel_t *channel;
  snd_mixer_channel_t device;

  mixer = mfile -> mixer;
  channel = snd_mixer_oss_channel( mixer, oss_device );
  if ( !channel ) return -ENXIO;
  device.channel = channel -> channel;
  fs = get_fs();
  set_fs( get_ds() );
  if ( (err = snd_mixer_device_read( mfile, &device )) < 0 ) {
    set_fs( fs );
    return err;
  }
  set_fs( fs );
  return (unsigned short)device.left |
         ((unsigned short)device.right << 8);
}

int snd_mixer_ioctl_card( snd_card_t *card, struct file *file, unsigned int cmd, unsigned long arg )
{
  snd_kmixer_file_t *mfile, sfile;
  snd_kmixer_t *mixer;
  int tmp;

  if ( card ) {
    /* note: this finds only first mixer for card */
    for ( tmp = 0; tmp < SND_MIXERS; tmp++ ) {
      mixer = snd_mixers[ tmp ];
      if ( mixer && mixer -> card == card ) break;
    }
    if ( !mixer ) return -ENXIO;
    memset( &sfile, 0, sizeof( sfile ) );
    sfile.mixer = mixer;
    mfile = &sfile;
  } else {
    mfile = (snd_kmixer_file_t *)file -> private_data;
    if ( !mfile ) return -EIO;
    mixer = mfile -> mixer;
  }
  if ( ( ( cmd >> 8 ) & 0xff ) == 'R' ) {
    switch ( cmd ) {
      case SND_MIXER_IOCTL_PVERSION:
        return snd_ioctl_out( (long *)arg, SND_MIXER_VERSION );
      case SND_MIXER_IOCTL_CHANNELS:
        return snd_ioctl_out( (long *)arg, mixer -> channels_count );
      case SND_MIXER_IOCTL_INFO:
        return snd_mixer_info( mixer, (snd_mixer_info_t *)arg );
      case SND_MIXER_IOCTL_EXACT:
        tmp = snd_ioctl_in( (long *)arg );
        mfile -> exact = tmp != 0;
        snd_ioctl_out( (long *)arg, mfile -> exact );
        return 0;
      case SND_MIXER_IOCTL_CHANNEL_INFO:
        return snd_mixer_device_info( mfile, (snd_mixer_channel_info_t *)arg );
      case SND_MIXER_IOCTL_CHANNEL_READ:
        return snd_mixer_device_read( mfile, (snd_mixer_channel_t *)arg );
      case SND_MIXER_IOCTL_CHANNEL_WRITE:
        return snd_mixer_device_write( mfile, (snd_mixer_channel_t *)arg );
      case SND_MIXER_IOCTL_SPECIAL_READ:
        return snd_mixer_special_read( mixer, (struct snd_mixer_special *)arg );
      case SND_MIXER_IOCTL_SPECIAL_WRITE:
        return snd_mixer_special_write( mixer, (struct snd_mixer_special *)arg );
    }
    return -ENXIO;
  }
  if ( ( ( cmd >> 8 ) & 0xff ) != 'M' ) return -ENXIO;
#if 0
  snd_printk( ">> mixer ioctl - cmd = 0x%x\n", cmd );
#endif
  if ( cmd == SND_MIXER_OSS_INFO )
    {
      struct snd_oss_mixer_info info;

      memset( &info, 0, sizeof( info ) );
      strncpy( info.id, mixer -> id, sizeof( info.id ) - 1 );
      strncpy( info.name, mixer -> name, sizeof( info.name ) - 1 );
      if ( verify_area( VERIFY_WRITE, (void *)arg, sizeof( snd_mixer_info ) ) ) return -EFAULT;
      copy_to_user( (void *)arg, &snd_mixer_info, sizeof( snd_mixer_info ) );
      return 0;
    }
  if ( cmd & IOC_IN )
    switch ( cmd ) {
      case SND_MIXER_OSS_SET_RECSRC:
        return snd_ioctl_out( (long *)arg, snd_mixer_set_recsrc( mfile, snd_ioctl_in( (long *)arg ) ) );
      default:
        tmp = snd_mixer_set_volume( mfile, cmd & 0xff, snd_ioctl_in( (long *)arg ) );
        return tmp >= 0 ? snd_ioctl_out( (long *)arg, tmp ) : tmp;
    }
   else
  if ( cmd & IOC_OUT )
    switch ( cmd ) {
      case SND_OSS_GETVERSION:
        return snd_ioctl_out( (long *)arg, SND_OSS_VERSION );
      case SND_MIXER_OSS_DEVMASK:
        return snd_ioctl_out( (long *)arg, snd_mixer_devmask( mixer ) );
      case SND_MIXER_OSS_STEREODEVS:
        return snd_ioctl_out( (long *)arg, snd_mixer_stereodevs( mixer ) );
      case SND_MIXER_OSS_RECMASK:
        return snd_ioctl_out( (long *)arg, snd_mixer_recmask( mixer ) );
      case SND_MIXER_OSS_CAPS:
        return snd_ioctl_out( (long *)arg, snd_mixer_osscaps( mixer ) );
      case SND_MIXER_OSS_RECSRC:
        return snd_ioctl_out( (long *)arg, snd_mixer_get_recsrc( mixer ) );
      default:
        tmp = snd_mixer_get_volume( mfile, cmd & 0xff );
        return tmp >= 0 ? snd_ioctl_out( (long *)arg, tmp ) : tmp;
    }
#ifdef SNDCFG_DEBUG
  snd_printk( "MIXER ERR\n" );
  snd_printk( "  MIXER REQUEST: 0x%x, 0x%x\n", cmd, (int)snd_ioctl_in( (long *)arg ) );
#endif
  return -ENXIO;
}

static int snd_mixer_control_ioctl( snd_card_t *card, snd_control_t *control, unsigned int cmd, unsigned long arg )
{
  switch ( cmd ) {
    case SND_CTL_IOCTL_HW_INFO:
      {
        struct snd_ctl_hw_info *ptr = (struct snd_ctl_hw_info *)arg;
        if ( snd_mixers[ (card -> number << 1) + 1 ] ) ptr -> mixerdevs = 2; else
        if ( snd_mixers[ (card -> number << 1) + 0 ] ) ptr -> mixerdevs = 1; else
        ptr -> mixerdevs = 0;
        return 0;
      }
    case SND_CTL_IOCTL_MIXER_DEVICE:
      {
        int val = snd_ioctl_in( (long *)arg );
        if ( val < 0 || val > 1 ) return -EINVAL;
        if ( !snd_mixers[ (card -> number << 1) + 1 ] && val > 1 ) return -EINVAL;
        if ( !snd_mixers[ (card -> number << 1) + 0 ] ) return -EINVAL;
        control -> mixer_device = val;
        return 0;
      }
    case SND_CTL_IOCTL_MIXER_INFO:
      return snd_mixer_info( snd_mixers[ (card -> number << 1) + control -> mixer_device ], (snd_mixer_info_t *)arg );
  }
  return -EAGAIN;
}

static int snd_mixer_ioctl( struct file *file, unsigned int cmd, unsigned long arg )
{
  return snd_mixer_ioctl_card( NULL, file, cmd, arg );
}

static long snd_mixer_read( struct file *file, char *buffer, long count )
{
  unsigned long flags;
  unsigned int device;
  unsigned char buf[ 8 ];
  snd_kmixer_file_t *mfile;
  snd_kmixer_t *mixer;
  long size = 0;

  mfile = (snd_kmixer_file_t *)file -> private_data;
  if ( !mfile ) return -ENOENT;
  mixer = mfile -> mixer;
  if ( !mixer ) return -ENOENT;
  if ( count < 8 ) return 0;
  memset( buf, 0, sizeof( buf ) );
  for ( device = 0; device < mfile -> mixer -> channels_count; device++ ) {
    snd_spin_lock( mixer, lock, &flags );
    if ( mfile -> changes & (1 << device) ) {
      mfile -> changes &= ~(1 << device);
      snd_spin_unlock( mixer, lock, &flags );
      *(unsigned int *)&buf[ 4 ] = device;
      copy_to_user( buffer, buf, 8 );
      buffer += 8;
      size += 8;
      count -= 8;
      if ( count < 8 ) return size;
    } else {
      snd_spin_unlock( mixer, lock, &flags );
    }
  }
  return size;
}

#ifdef SND_POLL
static unsigned int snd_mixer_poll( struct file *file, poll_table *wait )
{
  unsigned long flags;
  unsigned int mask;
  snd_kmixer_file_t *mfile;
  snd_kmixer_t *mixer;

  mfile = (snd_kmixer_file_t *)file -> private_data;
  if ( !mfile ) return -EIO;
  mixer = mfile -> mixer;
 
  snd_spin_lock( mixer, lock, &flags );
  snd_getlock( mfile, change ) |= SND_WK_SLEEP;
  snd_spin_unlock( mixer, lock, &flags );
  snd_poll_wait( file, mfile, change, wait );
  
  mask = 0;
  if ( mfile -> changes )
    mask |= POLLIN | POLLRDNORM;

  return mask;
}
#else
static int snd_mixer_select( struct file *file, int sel_type, select_table *wait )
{
  unsigned long flags;
  snd_kmixer_file_t *mfile;
  snd_kmixer_t *mixer;

  mfile = (snd_kmixer_file_t *)file -> private_data;
  if ( !mfile ) return -EIO;  
  mixer = mfile -> mixer;

  switch ( sel_type ) {
    case SEL_IN:
      snd_spin_lock( mixer, lock, &flags );
      if ( !mfile -> changes )
        {
          snd_getlock( mfile, change ) |= SND_WK_SLEEP;
          snd_spin_unlock( mixer, lock, &flags );
          snd_select_wait( mfile, change, wait );
          return 0;
        }
      snd_getlock( mfile, change ) &= ~SND_WK_SLEEP;
      snd_spin_unlock( mixer, lock, &flags );
      return 1;
    case SEL_OUT:
      break;
    case SEL_EX:
      break;
  }
  return 0;
}
#endif

/*
 *  /proc interface
 */

static void snd_mixer_proc_read( snd_info_buffer_t *buffer, void *private_data )
{
  snd_kmixer_t *mixer;
  snd_kmixer_channel_t *channel;
  int idx;
  
  mixer = (snd_kmixer_t *)private_data;
  snd_iprintf( buffer, "Mixer '%s'\n", mixer -> name );
  for ( idx = 0; idx < mixer -> channels_count; idx++ ) {
    channel = mixer -> channels[ idx ];
    if ( !channel ) continue;
    snd_iprintf( buffer, "Channel '%s' %d %d %d %d%s%s\n",
    	channel -> hw.name,
    	channel -> hw.min,
    	channel -> hw.max,
    	channel -> uleft,
    	channel -> uright,
    	channel -> umute & SND_MIX_MUTE_LEFT ? " lmute" : "",
    	channel -> umute & SND_MIX_MUTE_RIGHT ? " rmute" : "",
    	channel -> record ? " record" : "" );
  }
}

static void snd_mixer_proc_write( snd_info_buffer_t *buffer, void *private_data )
{
  snd_kmixer_t *mixer;
  snd_kmixer_channel_t *channel;
  char line[ 80 ];
  char str[ 80 ], *ptr;
  int idx, min, max, left, right, err;
  unsigned int mflags;
  snd_kmixer_file_t sfile;
  snd_mixer_channel_t device;
  mm_segment_t fs;

  mixer = (snd_kmixer_t *)private_data;
  if ( snd_info_get_line( buffer, line, sizeof( line ) ) ||
       strncmp( line, "Mixer ", 6 ) ) {
    buffer -> error = -EINVAL;
    return;
  }
  ptr = snd_info_get_str( str, line + 6, sizeof( str ) );
  if ( strcmp( mixer -> name, str ) ) {
    buffer -> error = -EINVAL;
    return;
  }
  while ( !snd_info_get_line( buffer, line, sizeof( line ) ) ) {
    if ( strncmp( line, "Channel ", 8 ) ) {
      buffer -> error = -EINVAL;
      return;
    }
    ptr = snd_info_get_str( str, line + 8, sizeof( str ) );
    for ( idx = 0; idx < mixer -> channels_count; idx++ ) {
      channel = mixer -> channels[ idx ];
      if ( !strcmp( channel -> hw.name, str ) ) {
        if ( *ptr == '-' ) {
          min = -(int)simple_strtoul( ptr + 1, &ptr, 10 );
        } else {
          min = (int)simple_strtoul( ptr, &ptr, 10 );
        }
        if ( *++ptr == '-' ) {
          max = -(int)simple_strtoul( ptr + 1, &ptr, 10 );
        } else {
          max = (int)simple_strtoul( ptr, &ptr, 10 );
        }
        if ( *++ptr == '-' ) {
          left = -(int)simple_strtoul( ptr + 1, &ptr, 10 );
        } else {
          left = (int)simple_strtoul( ptr, &ptr, 10 );
        }
        if ( *++ptr == '-' ) {
          right = -(int)simple_strtoul( ptr + 1, &ptr, 10 );
        } else {
          right = (int)simple_strtoul( ptr, &ptr, 10 );
        }
        if ( channel -> hw.min != min || channel -> hw.max != max ) {
          snd_printk( "invalid ranges for mixer '%s' and channel '%s', skipping\n", mixer -> name, channel -> hw.name );
          break;
        }
        if ( left < min || left > max || right < min || right > max ) {
          snd_printk( "invalid values for mixer '%s' and channel '%s', skipping\n", mixer -> name, channel -> hw.name );
          break;
        }
        mflags = 0;
        while ( 1 ) {
          ptr = snd_info_get_str( str, ptr, sizeof( str ) );
          if ( str[ 0 ] == '\0' ) break;
          if ( !strcmp( str, "lmute" ) ) mflags |= SND_MIXER_FLG_MUTE_LEFT; else
          if ( !strcmp( str, "rmute" ) ) mflags |= SND_MIXER_FLG_MUTE_RIGHT; else
          if ( !strcmp( str, "rec" ) ) mflags |= SND_MIXER_FLG_RECORD; else
          snd_printk( "uknown mixer channel option '%s'\n", str );
        }
        memset( &device, 0, sizeof( device ) );
        device.channel = idx;
        device.flags = mflags;
        device.left = left;
        device.right = right;
        memset( &sfile, 0, sizeof( sfile ) );
        sfile.mixer = mixer;
        sfile.exact = 1;
        fs = get_fs();
        set_fs( get_ds() );
        if ( (err = snd_mixer_device_write( &sfile, &device )) < 0 ) {
          snd_printk( "unable to set values for mixer '%s' and channel '%s' (%i)\n", mixer -> name, channel -> hw.name, err );
        }
        set_fs( fs );
        break;
      }
    }
    if ( idx >= mixer -> channels_count ) {
      buffer -> error = -EINVAL;
      return;
    }
  }
}

/*
 *  REGISTRATION PART
 */

static snd_minor_t snd_mixer_reg = {
  "mixer",

  NULL,					/* unregister */

  NULL,					/* lseek */
  snd_mixer_read,			/* read */
  NULL,					/* write */
  snd_mixer_open,			/* open */
  snd_mixer_release,			/* release */
#ifdef SND_POLL
  snd_mixer_poll,			/* poll */
#else
  snd_mixer_select,			/* select */
#endif
  snd_mixer_ioctl,			/* ioctl */
  NULL					/* mmap */
};

snd_kmixer_t *snd_mixer_new( snd_card_t *card, char *id )
{
  snd_kmixer_t *mixer;
  
  mixer = (snd_kmixer_t *)snd_malloc( sizeof( snd_kmixer_t ) );
  if ( !mixer ) return NULL;
  memset( mixer, 0, sizeof( snd_kmixer_t ) );
  mixer -> card = card;
  if ( id ) {
    strncpy( mixer -> id, id, sizeof( mixer -> id ) - 1 );
  }
  snd_mutex_prepare( mixer, ffile );
  snd_spin_prepare( mixer, lock );
  return mixer;
}

int snd_mixer_free( snd_kmixer_t *mixer )
{
  int idx;
  snd_kmixer_channel_t *channel;

  if ( !mixer ) return -EINVAL;
  for ( idx = 0; idx < mixer -> channels_count; idx++ ) {
    channel = mixer -> channels[ idx ];
    snd_free( channel, sizeof( snd_kmixer_channel_t ) );
  }
  if ( mixer -> private_data && mixer -> private_free )
    mixer -> private_free( mixer -> private_data );
  snd_free( mixer, sizeof( snd_kmixer_t ) );
  return 0;
}

snd_kmixer_channel_t *snd_mixer_new_channel( snd_kmixer_t *mixer, struct snd_stru_mixer_channel_hw *hw )
{
  int idx, idx1, priority;
  snd_kmixer_channel_t *channel;

  snd_mutex_down( mixer, ffile );
  if ( !mixer || mixer -> channels_count >= SND_MIXER_CHANNELS ) {
    snd_mutex_up( mixer, ffile );
    return NULL;
  }
  channel = (snd_kmixer_channel_t *)snd_malloc( sizeof( snd_kmixer_channel_t ) );
  if ( !channel ) {
    snd_mutex_up( mixer, ffile );
    return NULL;
  }
  memset( channel, 0, sizeof( snd_kmixer_channel_t ) );
  memcpy( &channel -> hw, hw, sizeof( struct snd_stru_mixer_channel_hw ) );
  priority = channel -> hw.priority;
  for ( idx = 0; idx < mixer -> channels_count; idx++ )
    if ( mixer -> channels[ idx ] -> hw.priority > priority ) break;
  for ( idx1 = mixer -> channels_count; idx1 > idx; idx1-- ) {
    mixer -> channels[ idx1 ] = mixer -> channels[ idx1 - 1 ];
  }
  mixer -> channels_count++;
  mixer -> channels[ idx ] = channel;
  for ( idx = 0; idx < mixer -> channels_count; idx++ ) {
    mixer -> channels[ idx ] -> channel = idx;
  }
  snd_mutex_up( mixer, ffile );
  return channel;
}

void snd_mixer_reorder_channel( snd_kmixer_t *mixer, snd_kmixer_channel_t *channel )
{
  int idx, idx1;

  if ( !mixer || !channel ) return;
  snd_mutex_down( mixer, ffile );
  for ( idx = 0; idx < mixer -> channels_count; idx++ ) {
    if ( mixer -> channels[ idx ] == channel ) {
      for ( idx1 = idx; idx1 + 1 < mixer -> channels_count; idx1++ )
        mixer -> channels[ idx1 ] = mixer -> channels[ idx1 + 1 ];
      mixer -> channels_count--;
      break;
    }
  }
  for ( idx = 0; idx < mixer -> channels_count; idx++ )
    if ( mixer -> channels[ idx ] -> hw.priority > channel -> hw.priority ) break;
  for ( idx1 = mixer -> channels_count; idx1 > idx; idx1-- ) {
    mixer -> channels[ idx1 ] = mixer -> channels[ idx1 - 1 ];
  }
  mixer -> channels[ idx ] = channel;
  mixer -> channels_count++;
  for ( idx = 0; idx < mixer -> channels_count; idx++ ) {
    mixer -> channels[ idx ] -> channel = idx;
  }
  snd_mutex_up( mixer, ffile );
}

int snd_mixer_register( snd_kmixer_t *mixer, int device )
{
  mm_segment_t fs;
  char name[16];
  int idx, err, cardnum;
  snd_info_entry_t *entry;
  snd_kmixer_channel_t *channel;
  snd_kmixer_file_t sfile;
  snd_mixer_channel_t mdevice;

  if ( device < 0 || device > 1 ) return -EINVAL;
  if ( !mixer -> card ) return -EINVAL;
  idx = ((cardnum = mixer -> card -> number) * SND_MINOR_MIXERS) + device;
  snd_mutex_down_static( register ); 
  if ( snd_mixers[ idx ] ) {
    snd_mutex_up_static( register );
    return -EBUSY;
  }
  mixer -> device = device;
  snd_mixers[ idx ] = mixer;
  if ( (err = snd_register_minor( SND_MINOR_MIXER + (mixer -> card -> number * SND_MINOR_MIXERS) + device, &snd_mixer_reg )) < 0 ) {
    snd_mutex_up_static( register );
    snd_mixers[ idx ] = NULL;
    return err;
  }
  if ( (idx & (SND_MINOR_MIXERS-1)) == 0 )
    snd_register_minor( (cardnum << 4) + SND_MINOR_OSS_MIXER, &snd_mixer_reg );
  if ( (idx & (SND_MINOR_MIXERS-1)) == 1 )
    snd_register_minor( (cardnum << 4) + SND_MINOR_OSS_MIXER1, &snd_mixer_reg );
  snd_mutex_up_static( register );
  sprintf( name, "mixer%i", device );
  if ( (entry = snd_info_create_entry( mixer -> card, name )) != NULL ) {
    entry -> private_data = mixer;
    entry -> mode = S_IFREG | S_IRUGO | S_IWUSR;
    entry -> t.text.read_size = 256 + (mixer -> channels_count * 60);
    entry -> t.text.read = snd_mixer_proc_read;
    entry -> t.text.write_size = entry -> t.text.read_size;
    entry -> t.text.write = snd_mixer_proc_write;
    if ( snd_info_register( entry ) < 0 ) {
      snd_info_free_entry( entry );
      entry = NULL;
    }
  }
  mixer -> proc_entry = entry;
  for ( idx = 0; idx < mixer -> channels_count; idx++ ) {
    channel = mixer -> channels[ idx ];
    if ( !channel ) continue;

    memset( &sfile, 0, sizeof( sfile ) );
    sfile.mixer = mixer;
    sfile.exact = 1;

    memset( &mdevice, 0, sizeof( mdevice ) );
    mdevice.channel = channel -> channel;
    mdevice.flags = SND_MIXER_FLG_MUTE | SND_MIXER_FLG_FORCE;
    mdevice.left = channel -> hw.min;
    mdevice.right = channel -> hw.min;
    fs = get_fs();
    set_fs( get_ds() );
    if ( (err = snd_mixer_device_write( &sfile, &mdevice )) < 0 ) {
      snd_printd( "snd_mixer_register: oops, can't setup channel '%s' (%i)!!!\n", channel -> hw.name, err );
      set_fs( fs );
      return err;
    }
    set_fs( fs );    
  }
  snd_info_restore_text( mixer -> proc_entry );
  return 0;
}

int snd_mixer_unregister( snd_kmixer_t *mixer )
{
  int idx, cardnum;

  if ( !mixer ) return -EINVAL;
  if ( mixer -> proc_entry ) {
    snd_info_store_text( mixer -> proc_entry );
    snd_info_unregister( mixer -> proc_entry );
    mixer -> proc_entry = NULL;
  }
  snd_mutex_down_static( register );
  idx = (mixer -> card -> number * SND_MINOR_MIXERS) + mixer -> device;
  if ( snd_mixers[ idx ] != mixer ) {
    snd_mutex_up_static( register );
    return -EINVAL;
  }
  snd_unregister_minor( SND_MINOR_MIXER + idx );
  if ( (idx & (SND_MINOR_MIXERS-1)) == 0 ) {
    cardnum = idx >> 1;
    snd_unregister_minor( (cardnum << 4) + SND_MINOR_OSS_MIXER );
  }
  if ( (idx & (SND_MINOR_MIXERS-1)) == 1 ) {
    cardnum = idx >> 1;
    snd_unregister_minor( (cardnum << 4) + SND_MINOR_OSS_MIXER1 );
  }
  snd_mixers[ idx ] = NULL;
  snd_mutex_up_static( register );
  return snd_mixer_free( mixer );
}

/*
 *  INIT PART
 */

snd_kmixer_channel_t *snd_mixer_find_channel( snd_kmixer_t *mixer, unsigned int priority )
{
  int idx;
  snd_kmixer_channel_t *channel;

  if ( !mixer ) return NULL;
  for ( idx = 0; idx < mixer -> channels_count; idx++ ) {
    channel = mixer -> channels[ idx ];
    if ( channel -> hw.priority == priority )
      return channel;
  }
  return NULL;
}

#if 0
int snd_mixer_channel_init( snd_kmixer_t *mixer, unsigned int priority, unsigned char left, unsigned char right, unsigned int flags )
{
  mm_segment_t fs;
  int err;
  snd_kmixer_file_t sfile, *mfile;
  snd_kmixer_channel_t *channel;
  snd_mixer_channel_t device;

  memset( &sfile, 0, sizeof( sfile ) );
  sfile.mixer = mixer;
  mfile = &sfile;
  channel = snd_mixer_find_channel( mixer, priority );
  if ( !channel ) {
    snd_printd( "snd_mixer_channel_init: Oops, device with priority 0x%x not found!!!\n", priority );
    return -ENODEV;
  }
  device.channel = channel -> channel;
  device.flags = flags | SND_MIXER_FLG_FORCE;
  device.left = (unsigned char)left;
  device.right = (unsigned char)right;
  fs = get_fs();
  set_fs( get_ds() );
  if ( (err = snd_mixer_device_write( mfile, &device )) < 0 ) {
    snd_printd( "snd_mixer_channel_init: Oops, can't setup device with priority 0x%x (%i)!!!\n", priority, err );
    set_fs( fs );
    return err;
  }
  set_fs( fs );
  return 0;
}
#endif

#ifndef LINUX_2_1
extern struct symbol_table snd_symbol_table_mixerexport;
#endif

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

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