/* rexima 1.4 - a curses-based (and command-line) mixer for Linux.
 * Copyright (C) 1996-2003 Russell Marks.
 *
 * This program is free software; you can redistribute it and/or modify
 * it under the terms of the GNU General Public License as published by
 * the Free Software Foundation; either version 2 of the License, or
 * (at your option) any later version.
 *
 * This program is distributed in the hope that it will be useful,
 * but WITHOUT ANY WARRANTY; without even the implied warranty of
 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
 * GNU General Public License for more details.
 *
 * You should have received a copy of the GNU General Public License
 * along with this program; if not, write to the Free Software
 * Foundation, Inc., 675 Mass Ave, Cambridge, MA 02139, USA.
 */

#include <stdio.h>
#include <string.h>
#include <stdlib.h>
#include <unistd.h>
#include <fcntl.h>
#include <curses.h>
#include <sys/ioctl.h>
#include <sys/soundcard.h>


#define REXIMA_VER	"1.4"


#define DEV_Y_START	3
#define DEV_X_START	4
#define DEV_X_DEVEND	(DEV_X_START+9)
#define DEV_X_BAR	(DEV_X_DEVEND+3)
#define DEV_X_PCNT	(DEV_X_BAR+55)
#define DEV_X_REC	(DEV_X_PCNT+5)


char dev_labels[SOUND_MIXER_NRDEVICES][80]=SOUND_DEVICE_LABELS;
char dev_names [SOUND_MIXER_NRDEVICES][80]=SOUND_DEVICE_NAMES;
int dev_line[SOUND_MIXER_NRDEVICES];


char *mixerdev="/dev/mixer";


/* equivalents of optopt, opterr, optind, and optarg */
int optnopt=0,optnerr=0,optnind=1;
char *optnarg=NULL;

/* holds offset in current argv[] value */
static int optnpos=1;


/* This routine assumes that the caller is pretty sane and doesn't
 * try passing an invalid 'optstring' or varying argc/argv.
 */
int getoptn(int argc,char *argv[],char *optstring)
{
char *ptr;

/* check for end of arg list */
if(optnind==argc || *(argv[optnind])!='-' || strlen(argv[optnind])<=1)
  return(-1);

if((ptr=strchr(optstring,argv[optnind][optnpos]))==NULL)
  return('?');		/* error: unknown option */
else
  {
  optnopt=*ptr;
  if(ptr[1]==':')
    {
    if(optnind==argc-1) return(':');	/* error: missing option */
    optnarg=argv[optnind+1];
    optnpos=1;
    optnind+=2;
    return(optnopt);	/* return early, avoiding the normal increment */
    }
  }

/* now increment position ready for next time.
 * no checking is done for the end of args yet - this is done on
 * the next call.
 */
optnpos++;
if(optnpos>strlen(argv[optnind]))
  {
  optnpos=1;
  optnind++;
  }

return(optnopt);	/* return the found option */
}



void die(char *str)
{
fprintf(stderr,"rexima: couldn't %s.\n",str);
exit(1);
}


void init(int *mixfd,int *existmask,int *canrecmask,int *isrecmask,
          int *stereomask)
{
if(((*mixfd)=open(mixerdev,O_RDWR))<0) die("open mixer device");
if(ioctl(*mixfd,SOUND_MIXER_READ_DEVMASK,existmask)==-1) die("ioctl");
if(ioctl(*mixfd,SOUND_MIXER_READ_RECMASK,canrecmask)==-1) die("ioctl");
if(ioctl(*mixfd,SOUND_MIXER_READ_RECSRC,isrecmask)==-1) die("ioctl");
/* this looks like a `recent' addition [in 1996 maybe ;-)] so be lenient */
if(ioctl(*mixfd,SOUND_MIXER_READ_STEREODEVS,stereomask)==-1) *stereomask=0;
}


void init_term()
{
initscr(); cbreak(); noecho();
keypad(stdscr,TRUE);
}


void uninit(int mixfd)
{
clear(); refresh();
echo(); nocbreak(); endwin();
putchar('\n');
close(mixfd);
}


void drawsel(int new,int old)
{
if(new!=old)
  {
  if(old>=0)
    {
    mvaddstr(DEV_Y_START+dev_line[old],DEV_X_START-3,"  ");
    mvaddstr(DEV_Y_START+dev_line[old],DEV_X_DEVEND,"  ");
    }
  
  if(new>=0)
    {
    mvaddstr(DEV_Y_START+dev_line[new],DEV_X_START-3,"->");
    mvaddstr(DEV_Y_START+dev_line[new],DEV_X_DEVEND,"<-");
    }
  }

/* this'll be the last thing before a refresh, so... */
move(DEV_Y_START+dev_line[new],0);
}


int mixer_getlevel_stereo(int mixfd,int dev)
{
int level=0;

ioctl(mixfd,MIXER_READ(dev),&level);
return(level);
}


int mixer_getlevel(int mixfd,int dev)
{
return(mixer_getlevel_stereo(mixfd,dev)&255);
}


void mixer_setlevel_stereo(int mixfd,int dev,int left,int right)
{
left+=256*right;
ioctl(mixfd,MIXER_WRITE(dev),&left);
}


void mixer_setlevel(int mixfd,int dev,int level)
{
mixer_setlevel_stereo(mixfd,dev,level,level);
}


void mixer_change(int mixfd,int dev,int add)
{
int level=mixer_getlevel(mixfd,dev)+add;

if(level<0) level=0;
if(level>100) level=100;
mixer_setlevel(mixfd,dev,level);
}


void mixer_rectoggle(int mixfd,int dev,int *isrecmask)
{
(*isrecmask)^=(1<<dev);
ioctl(mixfd,SOUND_MIXER_WRITE_RECSRC,isrecmask);
}


void mixer_recset(int mixfd,int dev,int *isrecmask,int on)
{
if(on)
  (*isrecmask)|= (1<<dev);
else
  (*isrecmask)&=~(1<<dev);
ioctl(mixfd,SOUND_MIXER_WRITE_RECSRC,isrecmask);
}


void drawlevel(int dev,int level)
{
char buf[60];
int f;

/* sanity check */
if(level<0) level=0;
if(level>100) level=100;

memset(buf,'=',51);
buf[51]=0;
buf[level/2]='|';
for(f=level/2+1;f<51;f++)
  buf[f]='-';

mvaddstr(DEV_Y_START+dev_line[dev],DEV_X_BAR+1,buf);

sprintf(buf,"%3d%%",level);
mvaddstr(DEV_Y_START+dev_line[dev],DEV_X_PCNT,buf);
}


void drawrec(int dev,int on)
{
mvaddch(DEV_Y_START+dev_line[dev],DEV_X_REC+1,on?'R':' ');
}


void setupframe(int existmask,int canrecmask,int *firstdevp,int *lastdevp)
{
int f;
int offset=0;

mvaddstr(0,36,"rexima");
mvaddstr(DEV_Y_START-1,DEV_X_BAR+1,
         "min  .    .    .    .    :    .    .    .    .  max");

/* we know existmask is non-zero by now (see main()) */
for(f=0;f<SOUND_MIXER_NRDEVICES;f++)
  {
  if(existmask&(1<<f))
    {
    if(offset==0) *firstdevp=f;
    *lastdevp=f;
    dev_line[f]=offset++;
    mvaddstr(DEV_Y_START+dev_line[f],DEV_X_START,dev_labels[f]);
    mvaddch(DEV_Y_START+dev_line[f],DEV_X_BAR,'[');
    mvaddch(DEV_Y_START+dev_line[f],DEV_X_BAR+52,']');
    if(canrecmask&(1<<f))
      mvaddstr(DEV_Y_START+dev_line[f],DEV_X_REC,"[ ]");
    }
  }

mvaddstr(LINES-1,2,
         "< move/alter with hjkl/cursors; space to toggle rec src; "
         "Esc/q/x to quit >");
}



void usage_help(int existmask)
{
int f,count;

puts(
"rexima " REXIMA_VER " - copyright (c) 1996-2003 Russell Marks.\n"
"\n"
"usage: rexima [-hv] [-d mixer_device_file]\n"
"\t\t    [device <level | offset | left,right | <rec | norec>>\n"
"\t\t     [device ...]]\n"
"\n"
"	-d	specify mixer device file to use (ordinarily /dev/mixer).\n"
"	-h	give this usage help.\n"
"	-v	show current mixer settings.\n");
printf(
"	device	a device to set the level of. %s\n",mixerdev);
printf("\t\twill allow the levels of these devices to be set:\n\n\t\t");

count=0;
for(f=0;f<SOUND_MIXER_NRDEVICES;f++)
  {
  if(existmask&(1<<f))
    printf("%s ",dev_names[f]),count++;
  if(count>=7) printf("\n\t\t"),count=0;
  }

puts(
"\n\n"
"        level	level to set specified device to.\n"
"       offset	amount to change level by (e.g. `-3', `+12').\n"
"   left,right	set (stereo) device's level with independent left/right values.\n"
"  rec | norec	`rec' makes device a recording source, `norec' makes it, well,\n"
"		not a recording source. :-)\n"
"\n"
"If invoked without any args (with the exception of `-d'), rexima runs\n"
"interactively.");
}


/* on entry, we know argc>=2 */
void cmdline_main(int argc,char *argv[])
{
int mixfd;
int existmask,canrecmask,isrecmask,stereomask;
int f,tmp,l,r;
int found;
char *ptr;
int done=0,want_usage=0,want_levels=0;

do
  switch(getoptn(argc,argv,"d:hvV"))
    {
    case 'd':	/* mixer device */
      if((mixerdev=malloc(strlen(optnarg)+1))==NULL)
        die("allocate memory");
      strcpy(mixerdev,optnarg);
      break;
    case 'h':
      want_usage=1;
      break;
    case 'v': case 'V':	/* show levels */
      want_levels=1;
      break;
    case '?':
      switch(optnopt)
        {
        case 'd':
          fprintf(stderr,"rexima: "
                  "the `-d' option needs a mixer device to be specified.\n");
          break;
        default:
          fprintf(stderr,"rexima: option `%c' not recognised.\n",optnopt);
        }
      exit(1);
    case -1:
      done=1;
    }
while(!done);


if(want_usage)
  {
  init(&mixfd,&existmask,&canrecmask,&isrecmask,&stereomask);
  
  usage_help(existmask);
  
  exit(0);
  }

if(want_levels)
  {
  init(&mixfd,&existmask,&canrecmask,&isrecmask,&stereomask);
  
  for(f=0;f<SOUND_MIXER_NRDEVICES;f++)
    if(existmask&(1<<f))
      {
      tmp=mixer_getlevel_stereo(mixfd,f);
      l=(tmp&255); r=((tmp>>8)&255);
      printf("%s\t%3d",dev_names[f],l);
      if(stereomask&(1<<f)) printf(",%3d",r);
      if(canrecmask&(1<<f))
        printf("\t[%c]",(isrecmask&(1<<f))?'R':' ');
      putchar('\n');
      }
  
  exit(0);
  }

/* if there aren't any "device, setting" pairs, return (without init). */
if(optnind>=argc)
  return;

/* otherwise, init - we have one or more pairs to deal with. */

init(&mixfd,&existmask,&canrecmask,&isrecmask,&stereomask);

while(optnind<argc)
  {
  /* lookup argv[1] in dev_names[] */
  found=0;
  for(f=0;f<SOUND_MIXER_NRDEVICES;f++)
    if(existmask&(1<<f) && strcmp(argv[optnind],dev_names[f])==0)
      {
      found=1;
      break;
      }
  
  if(!found)
    {
    fprintf(stderr,"rexima: unavailable or unknown device `%s'.\n",argv[optnind]);
    exit(1);
    }
  
  if(argc-optnind<2)
    {
    fprintf(stderr,"rexima: no setting specified for device `%s'.\n",argv[optnind]);
    exit(1);
    }
  
  optnind++;
  
  tmp=f;	/* save dev no. */
  
  /* setting can be mono, stereo, or rec/norec.
   * first see if it's rec or norec:
   */
  if(strcmp(argv[optnind],"rec")==0 || strcmp(argv[optnind],"norec")==0)
    {
    if(!(canrecmask&(1<<f)))
      {
      fprintf(stderr,"rexima: can't set rec/norec on device `%s'.\n",
              argv[optnind-1]);
      exit(1);
      }
    
    mixer_recset(mixfd,tmp,&isrecmask,(argv[optnind][0]=='r'));
    }
  else
    {
    int offset_sign=0;
    
    /* see if it's an offset */
    if(argv[optnind][0]=='+')
      offset_sign=1;
    if(argv[optnind][0]=='-')
      offset_sign=-1;

    /* be sure to puke if level contains funny chars */
    for(f=(offset_sign?1:0);f<strlen(argv[optnind]);f++)
      if(strchr("0123456789, \t",argv[optnind][f])==NULL)
        {
        fprintf(stderr,"rexima: invalid level for device `%s'.\n",
                argv[optnind-1]);
        exit(1);
        }
    
    /* now check for stereo */
    if((ptr=strchr(argv[optnind],','))!=NULL)
      {
      if(offset_sign)
        {
        fprintf(stderr,"rexima: level offset must be mono, not stereo.\n");
        exit(1);
        }
      
      l=atoi(argv[optnind]);
      r=atoi(ptr+1);
      mixer_setlevel_stereo(mixfd,tmp,l,r);
      }
    else
      {
      /* must be mono; default to 2 for an offset with no size specified. */
      if(offset_sign)
        mixer_change(mixfd,tmp,
                     (strlen(argv[optnind])<2)?
                     2*offset_sign:atoi(argv[optnind]));
      else
        mixer_setlevel(mixfd,tmp,atoi(argv[optnind]));
      }
    }
    
  optnind++;
  }

exit(0);
}



int main(int argc,char *argv[])
{
int quit=0;
int cursel;	/* currently selected device */
int oldsel;
int mixfd;
int key;
int existmask,canrecmask,isrecmask,stereomask;
int firstdev,lastdev;
int f;

if(argc>1)
  cmdline_main(argc,argv);  /* returns (without init) if we're interactive */

init(&mixfd,&existmask,&canrecmask,&isrecmask,&stereomask);

/* later things assume existmask is non-zero, so... */
if(!existmask)
  fprintf(stderr,"rexima: mixer has no devices!\n"),exit(1);

init_term();
setupframe(existmask,canrecmask,&firstdev,&lastdev);
cursel=firstdev;

for(f=0;f<SOUND_MIXER_NRDEVICES;f++)
  if(existmask&(1<<f))
    {
    drawlevel(f,mixer_getlevel(mixfd,f));
    if(canrecmask&(1<<f))
      drawrec(f,isrecmask&(1<<f));
    }

drawsel(cursel,-1);
refresh();

while(!quit)
  {
  oldsel=cursel;
  
  key=getch();
  
  switch(key)
    {
    case 'q': case 'x': case 27:
      quit=1; break;
    case 'k': case KEY_UP:
      do
        cursel--;
      while(cursel>=firstdev && !(existmask&(1<<cursel)));
      if(cursel<firstdev) cursel=lastdev;
      break;
    case 'j': case KEY_DOWN:
      do
        cursel++;
      while(cursel<=lastdev && !(existmask&(1<<cursel)));
      if(cursel>lastdev) cursel=firstdev;
      break;
    case 'h': case 'H': case KEY_LEFT: case '-':
      if(existmask&(1<<cursel))
        {
        mixer_change(mixfd,cursel,(key=='H')?-1:-2);
        drawlevel(cursel,mixer_getlevel(mixfd,cursel));
        }
      break;
    case 'l': case 'L': case KEY_RIGHT: case '+': case '=':
      if(existmask&(1<<cursel))
        {
        mixer_change(mixfd,cursel,(key=='L')?1:2);
        drawlevel(cursel,mixer_getlevel(mixfd,cursel));
        }
      break;
    case ' ':
      if(canrecmask&(1<<cursel))
        {
        mixer_rectoggle(mixfd,cursel,&isrecmask);
        drawrec(cursel,isrecmask&(1<<cursel));
        }
      break;
    case 'R'-0x40: case 'L'-0x40:	/* ^R and ^L */
      clearok(curscr,TRUE);
      break;
    }
  
  drawsel(cursel,oldsel);
  
  refresh();
  }

uninit(mixfd);
exit(0);
}
