/*
 *      cmix: Friendly Command-line mAudio Mixer
 *
 *      Copyright 1997-2002 Stéphane Doyon <s.doyon@videotron.ca>
 */
#define VERSION "cmix v1.6\n"
/*
 *  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., 59 Temple Place, Suite 330, Boston, MA  02111-1307  USA
 */

#include <stdio.h>
#include <sys/types.h>
#include <fcntl.h>
#include <unistd.h>
#include <stdlib.h>
#include <sys/ioctl.h>
#include <errno.h>
#include <string.h>
#include <linux/soundcard.h>

/* special device-specific commands */
typedef struct special_command_t {
  char *names; /* names (aliases) for the command */
  int cmd; /* ioctl command code */
  char *mix_ids; /* mixer (card) IDs that support this semantic of the command. */
  int flags, query;
} special_command_t;

/* Multiple names or IDs are all in one string, separated by '|'. */

/* flags */
#define FL_BOOL 0x01 /* parameter is boolean: 0->off, 1->on. */
#define FL_HAS_QUERY 0x02 /* param is boolean + one value for "don't
			     change"/query, specified in command->query */
#define FL_NO_RETURN 0x04 /* do not bother showing arg after return, it won't
			     have been changed. */
#define FL_DONT_LIST 0x08 /* don't show in command listing */

special_command_t special_commands[] = {
  /* AGC and 3DSE are specifically documented in soundcard.h, therefore I
     won't limit there applications to only some cards, even though obviously
     few cards do support them.  */
  {"agc", SOUND_MIXER_AGC, NULL, FL_BOOL|FL_NO_RETURN, 0 },
  {"3dse|3d", SOUND_MIXER_3DSE, NULL, FL_BOOL|FL_HAS_QUERY, 2 },
  /* ony SB uses 3dse, but I'd hope to be generic... except using arg==2
     for query isn't documented anywhere and can't be assumed to be generic.
     Ah well... */
  {"priv1", SOUND_MIXER_PRIVATE1, NULL, FL_DONT_LIST, 0},
  {"priv2", SOUND_MIXER_PRIVATE2, NULL, FL_DONT_LIST, 0},
  {"priv3", SOUND_MIXER_PRIVATE3, NULL, FL_DONT_LIST, 0},
  {"priv4", SOUND_MIXER_PRIVATE4, NULL, FL_DONT_LIST, 0},
  {"priv5", SOUND_MIXER_PRIVATE5, NULL, FL_DONT_LIST, 0},
  /* ES1370 special commands */
  {"preamp|micboost"/*??*/, SOUND_MIXER_PRIVATE1, "ES1370",
        FL_BOOL|FL_HAS_QUERY, 0xFFFFFFFF },
  {"4chan|4channels", SOUND_MIXER_PRIVATE2, "ES1370",
        FL_BOOL|FL_HAS_QUERY, 0xFFFFFFFF },
  {"micbias", SOUND_MIXER_PRIVATE3, "ES1370",
        FL_BOOL|FL_HAS_QUERY, 0xFFFFFFFF },
  /* Feel free to add more! */
  {NULL, 0, NULL, 0, 0}
};

char *progname;

char *mix_dev = "/dev/mixer";
int mix_fd = -1;

#define NDEVS SOUND_MIXER_NRDEVICES

char *labels[] = SOUND_DEVICE_LABELS;
char *names[] = SOUND_DEVICE_NAMES;

int devs, recable, outable, stereo, caps;
mixer_info info;

void die(char *syscall)
{
  perror(syscall);
  exit(1);
}

void Open()
{
  if((mix_fd = open(mix_dev, O_RDWR)) <0) die("open");
  if(ioctl(mix_fd, SOUND_MIXER_READ_DEVMASK, &devs)==-1)
    die("SOUND_MIXER_READ_DEVMASK");
  if(ioctl(mix_fd, SOUND_MIXER_READ_RECMASK, &recable)==-1)
    die("SOUND_MIXER_READ_RECMASK");
  if(ioctl(mix_fd, MIXER_READ(SOUND_MIXER_OUTMASK), &outable)==-1)
    /* This is not supported on all devices apparently, so it is not an error
       if this fails. */
    outable = -1;
  if(ioctl(mix_fd, SOUND_MIXER_READ_STEREODEVS, &stereo)==-1)
    die("SOUND_MIXER_READ_STEREODEVS");
  if(ioctl(mix_fd, SOUND_MIXER_READ_CAPS, &caps)==-1)
    die("SOUND_MIXER_READ_CAPS");
}

void Close()
{
  close(mix_fd);
  mix_fd=-1;
}

int GetRecSrc()
{
  int mask;
  if(ioctl(mix_fd, SOUND_MIXER_READ_RECSRC, &mask)==-1)
    die("SOUND_MIXER_READ_RECSRC");
  return mask;
}

int GetOutSrc()
{
  int mask;
  if(outable == -1) return -1;
  if(ioctl(mix_fd, MIXER_READ(SOUND_MIXER_OUTSRC), &mask)==-1)
    die("MIXER_READ(SOUND_MIXER_OUTSRC)");
  return (mask & devs);
}

mixer_info *GetInfo()
{
  if(ioctl(mix_fd, SOUND_MIXER_INFO, &info)==-1)
    die("SOUND_MIXER_INFO");
  return &info;
}

void SetRecSrc(int mask)
{
  if(ioctl(mix_fd, SOUND_MIXER_WRITE_RECSRC, &mask)==-1) die("SOUND_MIXER_READ_RECSRC");
}

void SetOutSrc(int mask)
{
  if(ioctl(mix_fd, MIXER_WRITE(SOUND_MIXER_OUTSRC), &mask)==-1)
    die("MIXER_READ(SOUND_MIXER_OUTSRC)");
}

int DoSpecialCommand(int cmd, int arg)
/* For AGC, 3dSE and PRIVATE# commands. In all cases I've seen the arg
   is an int, and this is all we'll support (no structs!).
   Often, but not always, arg is replaced by a confirmed value.
   Often there is a "don't change" value: this allows queries as on return,
   arg was replaced by the setting that the command affects.
   AGC and 3DSE use 0->off, 1->on, 3DSE on SB uses 2->query, and no one
   else has 3dSE yet.
   ES1370 uses 0->off, !=0->on but -1=FFFFFFFF for query.
   Other cases are non-boolean: bitmasks or bitfields.
*/
{
  if(ioctl(mix_fd, cmd, &arg)==-1) {
    fprintf(stderr, "Error: %s\n",
	    strerror(errno));
    exit(1);
  }
  return arg;
}

void Get(int c, int *l, int *r)
{
  int v;
  if(ioctl(mix_fd, MIXER_READ(c), &v)==-1) die("MIXER_READ");
  *l = v & 0xFF;
  *r = (v >> 8) & 0xFF;
}

void Set(int c, int l, int r)
{
  int v;
  if(!(stereo & (1<<c)) && l!=r)
    { fprintf(stderr,"Unbalanced setting for mono channel %s\n",
	      names[c]);
      exit(1);
    }
  v = ((r & 0xFF)<<8) + (l & 0xFF);
  if(ioctl(mix_fd, MIXER_WRITE(c), &v)==-1) die("MIXER_WRITE");
}

void PrintInfo(mixer_info *info)
{
  info->id[15] = 0;
  info->name[31] = 0;
  info->fillers[9] = 0;
  printf("%s (%s): %d modifications\n",
	 info->id, info->name, info->modify_counter);
}

void PrintDev(int dev)
{
  int left, right;
  if(recable & (1<<dev)) printf("R");
  else printf(" ");
  if(outable != -1) {
    if(outable & (1<<dev)) printf(">");
    else printf(" ");
  }
  printf(" %s:  ", labels[dev]);
  Get(dev,&left,&right);
  if((stereo & (1<<dev)) && left!=right)
    printf("%d,%d", left,right);
  else printf("%d", left);
  if(!(stereo & (1<<dev)))
    printf(" (mono)");
  printf("\n");
}

void DumpPrintDev(int dev, FILE *fid)
{
  int left, right;
  fprintf(fid,"%s ", labels[dev]);
  Get(dev,&left,&right);
  if((stereo & (1<<dev)) && left!=right)
    fprintf(fid,"%d,%d", left,right);
  else fprintf(fid,"%d", left);
  fprintf(fid,"\n");
}

void PrintRecSrc()
{
  int i, mask;
  printf("Current recording sources:");
  mask = GetRecSrc();
  for(i=0; i<NDEVS; i++)
    if(mask & (1<<i))
      printf(" %s", names[i]);
  printf("\n");
}

void PrintOutSrc()
{
  int i, mask;
  if(outable == -1) return;
  printf("Current output sources:");
  mask = GetOutSrc();
  for(i=0; i<NDEVS; i++)
    if(mask & (1<<i))
      printf(" %s", names[i]);
  printf("\n");
}

void DumpPrintRecSrc(FILE *fid)
{
  int i, mask;
  fprintf(fid,"recsrc");
  mask = GetRecSrc();
  for(i=0; i<NDEVS; i++)
    if(mask & (1<<i))
      fprintf(fid," %s", names[i]);
  fprintf(fid,"\n");
}

void DumpPrintOutSrc(FILE *fid)
{
  int i, mask;
  if(outable == -1) return;
  fprintf(fid,"outsrc");
  mask = GetOutSrc();
  for(i=0; i<NDEVS; i++)
    if(mask & (1<<i))
      fprintf(fid," %s", names[i]);
  fprintf(fid,"\n");
}

void ListAll()
{
  int i;
  PrintInfo(GetInfo());
  for(i=0; i<NDEVS; i++)
    if(devs & (1<<i))
      PrintDev(i);
  printf("\n");
  if(caps & SOUND_CAP_EXCL_INPUT)
    printf("Only one recording source at a time.\n");
  else printf("Multiple simultaneous recording sources allowed.\n");
  PrintRecSrc();
  PrintOutSrc();
}

void DumpFile(char *name)
{
  int i;
  FILE *fid;
  if(!strcmp(name,"-")) fid = stdout;
  else if((fid = fopen(name,"w")) == 0) die("fopen");
  for(i=0; i<NDEVS; i++)
    if(devs & (1<<i))
      DumpPrintDev(i,fid);
  fprintf(fid,"\n");
  DumpPrintRecSrc(fid);
  DumpPrintOutSrc(fid);
  if(strcmp(name,"-")) fclose(fid);
}

int WhichDev(char *name)
{
  int i, dev=-1, len, l1,l2;
  len = strlen(name);
  if(len>0) {
    for(i=0; i<NDEVS; i++){
      if(devs & (1<<i)){
	l1 = strlen(labels[i]);
	l2 = strlen(names[i]);
	if((len<=l1 && !strncmp(name,labels[i],len))
	   || (len<=l2 && !strncmp(name,names[i],len))){
	  if(len==l1 || len==l2) return(i);
	  else if(dev == -1) dev = i;
	}
      }
    }
  }
  if(dev==-1) {
    fprintf(stderr,"Channel '%s' not recognized.\n", name); exit(1);
  }
  return(dev);
}

int IsTokenInString(char *str, char *sea)
/* is sea one of the | delimited tokens in str? */
{
  char *ptr;
  while(*str) {
    int len;
    ptr = strchr(str, '|');
    if(ptr)
      len = ptr-str;
    else len = strlen(str);
    if(!strncmp(str,sea,len))
      return 1;
    if(ptr)
      str = ptr+1;
    else return 0;
  }
  return 0;
}

char *FirstToken(char *str)
/* return the first token (substring until delimiter '|'). It's placed in a
   temporary static buf. */
{
  static char buf[128];
  char *ptr = strchr(str, '|');
  int len = (ptr) ? ptr-str : strlen(str);
  if(len>128-1) {
    fprintf(stderr,"Internal static buffer overflow!!\n");
    exit(1);
  }
  strncpy(buf, str, len);
  buf[len] = 0;
  return buf;
}

void ListSpecialCommands()
{
  special_command_t *cmd = special_commands;
  GetInfo();
  printf("Special commands possibly available on device %s:\n", info.id);
  while(cmd->names) {
    if( !(cmd->flags & FL_DONT_LIST)
	&& ( !cmd->mix_ids || IsTokenInString(cmd->mix_ids, info.id) )) {
      printf("  %s", FirstToken(cmd->names));
      if( (cmd->flags & FL_BOOL) ) {
	printf(" (bool");
	if( (cmd->flags & FL_HAS_QUERY) )
	  printf(", query with 0x%x", cmd->query);
	printf(")");
      }
      printf("\n");
    }
    cmd++;
  }
}

int GetPair(char *dat, int *left, int *right)
{
  char *endptr;
  int n;
  n = strtoul(dat, &endptr, 10);
  if(endptr == dat || n<0 || n>100) return(0);
  if(*endptr == 0){
    *left = *right = n;
    return(1);
  }
  if(*endptr != ',') return(0);
  *left = n;
  dat = endptr+1;
  n = strtoul(dat, &endptr, 10);
  if(*endptr != 0 || n<0 || n>100) return(0);
  *right = n;
  return(1);
}  

void UsageSpecialCommands()
{
  fprintf(stderr,VERSION);
  fprintf(stderr,"Device Dependent Commands Usage:\n\n");
  fprintf(stderr,"   %s - list -> List possibly available commands\n", progname);
  fprintf(stderr,"\n");
  fprintf(stderr,"   %s <command> [<argument>] -> Execute a special command\n", progname);
  fprintf(stderr,"\n");
  fprintf(stderr,"   <argument> can be :\n");
  fprintf(stderr,"   - A 32bits number, in decimal or, if it starts with 0x, in hex.\n");
  fprintf(stderr,"   - on (->1) or off (->0)\n");
  fprintf(stderr,"   - left out or 'q' to query (if the special query value\n"
	  "        for this command is known.\n");
  fprintf(stderr,"\n");
  fprintf(stderr,"   Generic commands priv1 -- priv5 are recognized.\n");
  exit(2);
}

void Usage()
{
  fprintf(stderr,VERSION);
  fprintf(stderr,"Usage:\n\n");
  fprintf(stderr,"   %s list -> Listing of all channels\n", progname);
  fprintf(stderr,"\n");
  fprintf(stderr,"   %s recsrc [<channel> ...] -> Get/set recording source\n", progname);
  fprintf(stderr,"   %s outsrc [<channel> ...] (not all cards) -> Get/set output source\n", progname);
  fprintf(stderr,"   %s - (a dash) -> Use device/driver-specific special commands\n", progname);
  fprintf(stderr,"   %s info -> General driver info\n", progname);
  fprintf(stderr,"\n");
  fprintf(stderr,"   %s <channel> {<left>[,<right>]] ...\n",progname);
  fprintf(stderr,"   %s <channel> -> Get level\n", progname);
  fprintf(stderr,"   %s <channel> <val> -> Set level to <val> (left = right = val)\n", progname);
  fprintf(stderr,"   %s <channel> <left>,<right> -> Set unbalanced stereo channel levels\n", progname);
  fprintf(stderr,"\n");
  fprintf(stderr,"   %s file <file> -> Read commands from file\n", progname);
  fprintf(stderr,"   %s dump <file> -> Dump current settings to command file\n", progname);
  fprintf(stderr,"\n");
  fprintf(stderr,"   Levels are between 0(off) and 100(max).\n");
  fprintf(stderr,"   Channels marked with 'R' are recordable.\n");
  fprintf(stderr,"   Channels marked with '>' are output sources.\n");
  fprintf(stderr,"\n");
  fprintf(stderr,"   %s mixdev <mixer_device> ... to use an alternate mixer device.\n", progname);
  exit(2);
}

void Parse(char *words[]);

void ReadFile(char *name)
{
  FILE *fid;
  char buf[301], *ptr, *words[50];
  int nw;
  if(!strcmp(name,"-")) fid = stdin;
  else if((fid = fopen(name,"r")) == 0) die("fopen");
  while(fgets(buf,300,fid) != NULL){
    buf[300] = 0;
    ptr = buf;
    while(ptr-buf<300 && (*ptr==' ' || *ptr=='\t')) ptr++;
    if(*ptr == '#') continue;
    nw = 0;
    while(1){
      words[nw++] = ptr;
      while(ptr-buf<300 && *ptr!=' ' && *ptr!='\t' && *ptr!=0
	    && *ptr!=10 && *ptr!=13)
	ptr++;
      if(*ptr==' ' || *ptr=='\t'){
	*(ptr++) = 0;
	while(ptr-buf<300 && (*ptr==' ' || *ptr=='\t')) ptr++;
      }else{
	if(ptr == words[nw-1]) nw--;
	*ptr=0;
        break;
      }
    }
    words[nw] = 0;
    Parse(words);
  }
  if(strcmp(name,"-")) fclose(fid);
}

void Parse(char *words[])
{
  if(words[0] == NULL) return;
  if(words[1] == NULL && (!strcmp(words[0], "-l")
		   || !strcmp(words[0], "list")))
    ListAll();

  else if(!strcmp(words[0], "-r") || !strcmp(words[0], "recsrc")){
    if(words[1] == NULL) PrintRecSrc();
    else{
      char **ptr = words+1;
      int mask = 0, d;
      while(*ptr){
	d = WhichDev(*ptr);
	if(!(recable & (1<<d)))
	  { fprintf(stderr, "%s is not a possible recording source.\n",
		    names[d]);
	    exit(1);
	  }
	mask |= (1<<d);
	ptr++;
      }
      SetRecSrc(mask);
      PrintRecSrc();
    }
  }

  else if(!strcmp(words[0], "-o") || !strcmp(words[0], "outsrc")){
    if(outable == -1) {
      fprintf(stderr, "Output sources are not supported on this device.\n");
      exit(1);
    }
    if(words[1] == NULL) PrintOutSrc();
    else{
      char **ptr = words+1;
      int mask = 0, d;
      while(*ptr){
	d = WhichDev(*ptr);
	if(!(outable & (1<<d)))
	  { fprintf(stderr, "%s is not a possible output source.\n",
		    names[d]);
	    exit(1);
	  }
	mask |= (1<<d);
	ptr++;
      }
      SetOutSrc(mask);
      PrintOutSrc();
    }
  }

  else if(!strcmp(words[0], "-i") || !strcmp(words[0], "info")){
    if(words[1] != NULL) Usage();
    PrintInfo(GetInfo());
  }

  else if(!strcmp(words[0], "-")) {
    if(words[1] == NULL) UsageSpecialCommands();
    if(words[2] == NULL && !strcmp(words[1], "list"))
      ListSpecialCommands();
    else{
      int val;
      special_command_t *cmd = special_commands;
      GetInfo();
      while(cmd->names) {
	if(IsTokenInString(cmd->names, words[1])) {
	  if(cmd->mix_ids && !IsTokenInString(cmd->mix_ids, info.id)) {
	    fprintf(stderr, 
		    "Command name '%s' is specific to another device.\n",
		    words[1]);
	    exit(1);
	  }
	  break;
	}
	cmd++;
      }
      if(!cmd->names) {
	fprintf(stderr, "Special command '%s' is unrecognized.\n", words[1]);
	exit(1);
      }
      if(words[2] == NULL || !strcmp(words[2], "q")
	 || !strcmp(words[2], "query") || !strcmp(words[2], "?")) {
	   if( !(cmd->flags & FL_HAS_QUERY) ) {
	     fprintf(stderr, "Don't know how to query for this command: specify an argument.\n");
	     exit(1);
	   }
	   val = cmd->query;
      }else if(!strcmp(words[2], "on"))
	val = 1;
      else if(!strcmp(words[2], "off"))
	val = 0;
      else{
	char *endptr;
	val = strtoul(words[2], &endptr, 0);
	if(*endptr != 0) {
	  fprintf(stderr, "Unalbe to parse command argument.\n");
	  fprintf(stderr, "Arg should be: 'on' / 'off' or a number, "
		  "in decimal or in hex (prepend 0x)\n");
	  exit(1);
	}
      }
      printf("%s: ", words[1]);
      fflush(stdout);
      val = DoSpecialCommand(cmd->cmd, val);
      if( !(cmd->flags & FL_NO_RETURN) ) {
	if(words[2] != NULL && !strncmp(words[2], "0x", 2))
	  printf("%x", val);
	else printf("%d", val);
	printf("\n");
      }else printf("OK\n");
    }
  }

  else if(!strcmp(words[0], "-f") || !strcmp(words[0], "file")){
    if(words[1] == NULL) UsageSpecialCommands();
    ReadFile(words[1]);
  }

  else if(!strcmp(words[0], "-d") || !strcmp(words[0], "dump")){
    if(words[1] == NULL) Usage();
    DumpFile(words[1]);
  }

  else{
    char **ptr = words;
    int dev, left, right;
    while(*ptr){
      dev = WhichDev(*ptr);
      if(*(ptr+1) && GetPair(*(ptr+1),&left,&right)){
	Set(dev,left,right);
	ptr++;
      }
      PrintDev(dev);
      ptr++;
    }
  }
}

int main(int argc, char *argv[])
{
  progname = argv[0];

  argv++;
  if(argv[0] != NULL
     && (!strcmp(argv[0], "-m")
	 || !strcmp(argv[0], "mixdev"))) {
    if(argv[1] == NULL) Usage();
    mix_dev = argv[1];
    argv+=2;
  }

  if(argv[0] == NULL) Usage();

  Open();
  Parse(argv);
  Close();

  return 0;
}
