/*
 * (Unnamed) mixer - v0.01
 * Mixer for OSS/X11
 * Copyright 1998, Ben Buxton (bb@zip.com.au)
 *
 * compile with: gcc -o mixer mixer.c -L/usr/X11/lib -lX11
 *
 * Button1 over the letters selects the device show/changed by
 * the slider.
 * Button3 selects the input device.
 * Ctrl-Button2 moves the window around
 * Shift-Button2 quits
 *
 * check http://www.zip.com.au/~bb/linux/ for new versions
 *
 * 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 <X11/X.h>
#include <X11/Xlib.h>
#include <X11/Xutil.h>
#include <X11/Xresource.h>
#include <X11/Xatom.h>
#include <linux/soundcard.h>
#include <X11/cursorfont.h>
#include <fcntl.h>

#define WIDTH 35
#define HEIGHT 100
#define FONT "-adobe-helvetica-*-r-*-*-10-*-*-*-*-*-*-*"
#define VOLTEXT "V"
#define LINTEXT "L"
#define DSPTEXT "D"
#define CDTEXT "C"

void DoBevels();
void GetVolume();
void OpenMixer();
void DrawSlider();
void SetVolume();
void DrawTicks();
void DrawButtons();

Display *disp;
int screen;
Window win, root;
Bool sliding=False;
Bool override;
GC textagc, textbgc, topshad, botshad, tickgc, ingc, greygc;
XEvent event;

static XrmOptionDescRec opts[] = {
{"-geometry", ".geometry", XrmoptionSepArg, (caddr_t) NULL},
{"-display",  ".display",  XrmoptionSepArg, (caddr_t) NULL},
{"-device",   ".device",   XrmoptionSepArg, (caddr_t) NULL},
{"-override", ".override", XrmoptionNoArg, (caddr_t) "True"},
{"-font",     ".font",     XrmoptionSepArg, (caddr_t) NULL},
NULL
};

/* Dimensions */
int width=WIDTH, height=HEIGHT, x, y;
/* Slider dims */
int swidth, sheight;
int volume;
/* Total slider area height, button area height */
int sw, bw;

/* Info for 'buttons'*/
XTextProperty prop;
XFontStruct *font;
char *fontname = FONT;
int volht, linht, cdht, dspht;
int liney, cdy, voly, dspy;

/* Info on current mixer dev */
int mixerfd;
int device = 1; /* 1 = main, 2 = line, 4 = cd, 8 = dsp */
int unavail = 0; /* List of unavailable mixers (mask)*/
int devmask;
int numdev;
char *mixer;
int indevice; /* Input device */


void main(int argc, char **argv) {
Colormap cmap;
XGCValues vals;
unsigned long mask;
XColor texta, textb, in, topgrey, botgrey, grey, tick;
XSizeHints hints;
XTextProperty prop;
XSetWindowAttributes attributes;
Window rr, cr;
Bool moving = False;
int rx, ry, px, py, mr, wx, wy;
int tx, newh;
int gstatus = 0;
XrmDatabase rdb, srvdb = NULL, cmdb;
XrmValue val;
char *em = "";
char *str_type, buffer[50];
char *display = NULL;
int rwidth, rheight;
int d;

	/* Go through cmdline options, etc. Also connect to disp */

	XrmInitialize();

	cmdb = XrmGetStringDatabase(em);

	XrmParseCommand(&cmdb, opts, 5, "emix", &argc, argv);

	if (XrmGetResource(cmdb, "emix.display", "Emix.display", &str_type, &val))
	{
	    display = (void *)malloc(val.size+1);
	    strncpy(display, val.addr, val.size);
	}

	/* Open the display */
	disp = XOpenDisplay(display);
	if (disp == NULL)
	{
 	   printf("Error: Cannot open display %s!\n", display);
 	   exit(1);
	}

	if(XResourceManagerString(disp) != NULL)
	{
	    srvdb = XrmGetStringDatabase(XResourceManagerString(disp));
	}

	XrmMergeDatabases(srvdb, &cmdb);

	rdb = cmdb;

	if (XrmGetResource(rdb, "emix.geometry", "Emix.geometry", &str_type, &val))
	{
 	   strncpy(buffer, val.addr, (int) val.size);
 	   gstatus = XParseGeometry(buffer, &x, &y, &width, &height);
	    if((gstatus & XValue) || (gstatus & YValue))
 	           hints.flags = USPosition;
 	   if((gstatus & WidthValue) || (gstatus & HeightValue))
	            hints.flags |= USSize;
	}

	if (XrmGetResource(rdb, "emix.device", "Emix.device", &str_type, &val))
	{
 	   mixer = (void *)malloc(val.size+1);
	    strncpy(mixer, val.addr, val.size);
	} else {
	    mixer = "/dev/mixer";
	}
	if (XrmGetResource(rdb, "emix.override", "Emix.override", &str_type, &val))
	{
		if(!strncasecmp("true", val.addr, val.size))
			override = 1;
	}
	if (XrmGetResource(rdb, "emix.font", "Emix.font", &str_type, &val))
	{
  		fontname = (void *)malloc(val.size+1);
		strncpy(font, val.addr, val.size);
	}

	if(argc != 1) {
	    printf("Usage: %s [-dev mixer]\n", argv[0]);
	    exit(1);
	}

	/* Do stuff with '-geom' command line option */
	if( !(gstatus & WidthValue))
	    width = WIDTH;
	if( !(gstatus & HeightValue))
	    height = HEIGHT;

	if( !(gstatus & XValue))
	    x = 1;
	else if (gstatus & XNegative)
	    x = rwidth + x - width;
	if( !(gstatus & YValue))
	    y = 1;
	else if (gstatus & YNegative)
	    y = rheight + y - height;

	hints.x = x;
	hints.y = y;
	hints.min_width = WIDTH;
	hints.min_height = HEIGHT;
	hints.flags |= PMinSize;

    OpenMixer();
    GetVolume();

    screen = DefaultScreen(disp);
    cmap = DefaultColormap(disp, screen);
    root = RootWindow(disp, screen);

/* Do stuff with font, etc */
    font = XLoadQueryFont(disp, fontname);
    if(font == NULL)
    {
        printf("Can't load font %s!\n", fontname);
        exit(1);
    }
    volht = linht = cdht = dspht =font->max_bounds.ascent + font->max_bounds.descent;
    bw = font->max_bounds.width+2;
    sw = width - bw;
/*
    voly = font->max_bounds.ascent+2;
    liney = height*2/5;
    dspy = height*7/10;
    cdy = height-4;
*/
	
	numdev = 0;
	if (devmask & SOUND_MASK_VOLUME)
		numdev++;
	if (devmask & SOUND_MASK_LINE)
		numdev++;
	if (devmask & SOUND_MASK_PCM)
		numdev++;
	if (devmask & SOUND_MASK_CD)
		numdev++;

	d = (height - voly - 12)/(numdev-1);
	voly = volht;
	if (devmask & SOUND_MASK_LINE)
		liney = voly + d;
	else
		liney = voly;
	if (devmask & SOUND_MASK_PCM)
		dspy = liney + d;
	else
		dspy = liney;
	cdy = height-4;


/* Colours we will use */
    topgrey.red = topgrey.blue = topgrey.green = 40000;
    botgrey.red = botgrey.blue = botgrey.green = 10000;
    grey.red = grey.blue = grey.green = 18724;
    tick.red = tick.blue = tick.green = 50000;
    texta.red = texta.green = 65535;
    texta.blue = 10000;
    textb.red = 10000;
    textb.blue = 10000;
    textb.green = 10000;
	in.red = 5000;
	in.green = 60000;
	in.blue = 20000;
    XAllocColor(disp, cmap, &topgrey);
    XAllocColor(disp, cmap, &botgrey);
    XAllocColor(disp, cmap, &grey);
    XAllocColor(disp, cmap, &in);
    XAllocColor(disp, cmap, &tick);
    XAllocColor(disp, cmap, &texta);
    XAllocColor(disp, cmap, &textb);

/* Create main window */
    mask = CWOverrideRedirect | CWBackPixel | CWCursor;
    attributes.override_redirect = True;
    attributes.background_pixel = grey.pixel;
    attributes.cursor = XCreateFontCursor(disp, XC_top_left_arrow);
    win = XCreateWindow(disp, root, x, y, width, height, 0,
        CopyFromParent, CopyFromParent, CopyFromParent,
        mask, &attributes);
	/* Set the size hints for the window */
	XSetWMNormalHints(disp, win, &hints);
	/* Set the name of the app for the WM */
	prop.value = "Emix";
	prop.encoding = XA_STRING;
	prop.format = 8;
	prop.nitems = strlen(prop.value);
	XSetWMName(disp, win, &prop);
    XMapWindow(disp, win);

/* Create GCs */
    mask = GCForeground;
    vals.foreground = topgrey.pixel;
    topshad = XCreateGC(disp, win, mask, &vals);

    vals.foreground = botgrey.pixel;
    botshad = XCreateGC(disp, win, mask, &vals);

    vals.foreground = tick.pixel;
    tickgc = XCreateGC(disp, win, mask, &vals);

    vals.foreground = in.pixel;
    ingc = XCreateGC(disp, win, mask, &vals);
    vals.foreground = grey.pixel;
    greygc = XCreateGC(disp, win, mask, &vals);

    mask |= GCFont;
    vals.foreground = texta.pixel;
    vals.font = font->fid;
    textagc = XCreateGC(disp, win, mask, &vals);

    vals.foreground = textb.pixel;
    vals.font = font->fid;
    textbgc = XCreateGC(disp, win, mask, &vals);

    XSelectInput(disp, win, ButtonPressMask|ButtonReleaseMask|
                PointerMotionMask|EnterWindowMask|
                ExposureMask);
    swidth = sw*8/10;
    sheight = height/10;

    DoBevels(win, 0, 0, width, height, topshad, botshad);
    DrawTicks();

    GetVolume();
    DrawSlider();

/* Main event loop */
    for(;;) {
        XNextEvent(disp, &event);
        switch(event.type)
        {
        case EnterNotify:
            /* Update mixer vals */
            GetVolume();
            DrawSlider();
            break;
        case ButtonPress:
        /* Pointer button pressed */
            /* Ctrl-Button2 moves window */
            if(event.xbutton.button == Button2 &&
                event.xbutton.state == ControlMask)
            {
                XQueryPointer(disp, win, &rr,
                    &cr, &rx, &ry, &px, &py, &mr);
                XRaiseWindow(disp, win);
                moving = True;
            /* Button1 moves slider/buttons */
            } else if (event.xbutton.button == Button1 || event.xbutton.button == Button3)
            {
                XQueryPointer(disp, win, &rr,
                    &cr, &rx, &ry, &px, &py, &mr);
                /* Select mixer device */
                if (px > sw) {
                    if(py < liney-linht) {
						if (event.xbutton.button != Button1)
							break;
						if (devmask & SOUND_MASK_VOLUME)
							device = 1;
                    } else if (py < dspy-dspht) {
						if (event.xbutton.button == Button3) {
							indevice = SOUND_MASK_LINE;
							ioctl(mixerfd, SOUND_MIXER_WRITE_RECSRC, &indevice);
						}
						else
						if (devmask & SOUND_MASK_LINE)
							device = 2;
                    } else if (py < cdy-cdht) {
						if (event.xbutton.button != Button1)
							break;
						if (devmask & SOUND_MASK_PCM)
							device = 4;
                    } else if (py <= height) {
						if (event.xbutton.button == Button3) {
							indevice = SOUND_MASK_CD;
							ioctl(mixerfd, SOUND_MIXER_WRITE_RECSRC, &indevice);
						} else if (devmask & SOUND_MASK_CD)
							device = 8;
                    }
                    DrawButtons();
                    GetVolume();
                    DrawSlider();
                    break;
                }
                /* Begin slider movement */
                sliding = True;
                newh = height - py;
                volume = newh*100/height;
                SetVolume();
                DrawSlider();
            }
            /* Shift-button2 quits */
            else if(event.xbutton.button == Button2 &&
                event.xbutton.state == ShiftMask)
            {
                exit(0);
            }
            break;
        /* Button-release stops stuff happening */
        case ButtonRelease:
            if(moving) {
                moving = False;
            }
            if(sliding) {
                sliding = False;
                GetVolume();
                DrawSlider();
            }
            break;
        /* Handle moving of mouse with button1 events */
        case MotionNotify:
            if(moving) {
                while(XCheckTypedEvent(disp, MotionNotify, &event));
                XQueryPointer(disp,root,&rr,&cr,&rx,&ry,&wx,&wy,&mr);
                XMoveWindow(disp, win, wx-px, wy-py);
            }else if (sliding) {
                while(XCheckTypedEvent(disp, MotionNotify, &event));
                XQueryPointer(disp,win,&rr,&cr,&rx,&ry,&wx,&wy,&mr);
                newh = height - wy;
                volume = newh*100/height;
                if (volume < 0) volume = 0;
                if (volume > 100) volume = 100;
                SetVolume();
                DrawSlider();
            }
            break;
        /* redraw the window on exposure */
        case Expose:
            GetVolume();
            DrawSlider();
            DrawTicks();
            DoBevels(win, 0, 0, width, height, topshad, botshad);
            break;
        }

    }
}
/* Draw bevels */
void DoBevels(Window win, int x, int y, int w, int h, GC top, GC bot)
{
XPoint points1[3];
XPoint points2[3];

        /* Window bevel coordinates */
        points1[0].x = points1[1].x  = x;
        points1[1].y = points1[2].y = y;
        points1[0].y = y+h-1;
        points1[2].x = x+w-1;

        points2[0].x = x;
        points2[0].y = points2[1].y = y+h-1;
    XPoint points2[3];

        /* Window bevel coordinates */
        points1[0].x = points1[1].x  = x;
        points1[1].y = points1[2].y = y;
        points1[0].y = y+h-1;
        points1[2].x = x+w-1;

        points2[0].x = x;
        points2[0].y = points2[1].y = y+h-1;
        points2[1].x = points2[2].x = x+w-1;
        points2[2].y = y+1;
        XDrawLines(disp, win, top, points1, 3, CoordModeOrigin);
        XDrawLines(disp, win, bot, points2, 3, CoordModeOrigin);
}

void OpenMixer() {
    mixerfd = open(mixer, O_RDWR);
    if (mixerfd == -1) {
        perror("Can't open mixer");
        exit(1);
    }
    ioctl(mixerfd, MIXER_READ(SOUND_MIXER_DEVMASK), &devmask);
}

/* retrive current mixer volume */
void GetVolume()
{
int vol;
int res;

    switch(device) {
    case 1:
        res = ioctl(mixerfd, MIXER_READ(SOUND_MIXER_VOLUME), &vol);
        break;
    case 2:
        res = ioctl(mixerfd, MIXER_READ(SOUND_MIXER_LINE), &vol);
        break;
    case 4:
        res = ioctl(mixerfd, MIXER_READ(SOUND_MIXER_PCM), &vol);
        break;
    case 8:
        res = ioctl(mixerfd, MIXER_READ(SOUND_MIXER_CD), &vol);
    }
    if (res==-1) {
                /* An undefined mixer channel was accessed - mark it*/
		unavail |= device;
    }
/* Get current input source */
	res = ioctl(mixerfd, MIXER_READ(SOUND_MIXER_RECSRC), &indevice);
    volume = vol*100/32768;
}

void DrawSlider()
{
int sx, sy;

    sx = sw/10 + 1;
    sy = height - sheight - ((height-sheight)*volume/78);
/* Vertical groove plus clear area for slider */
    XClearArea(disp, win, sx, 1, swidth, height-2, False);
    DrawButtons();
    XDrawLine(disp, win, botshad, sw/2-1, sheight/2, sw/2-1, height-sheight/2);
    XDrawLine(disp, win, topshad, sw/2, sheight/2, sw/2, height-sheight/2);
    XFillRectangle(disp, win, tickgc, sx, sy, swidth, sheight);
/* Slider outer bevels */
    if(sliding)
        DoBevels(win, sx, sy, swidth, sheight, botshad, topshad);
    else
        DoBevels(win, sx, sy, swidth, sheight, topshad, botshad);
/* Thumb grip */
    XDrawLine(disp, win, botshad, 7, sy+(sheight/2), sw-8, sy+(sheight/2));
    XDrawLine(disp, win, topshad, 7, sy+(sheight/2)-1, sw-8, sy+(sheight/2)-1);

    XSync(disp, False);
}

/* Set mixer volume then retrive the actual resultant setting */
void SetVolume()
{
int vol;
int res;
unsigned char v;
    v = volume;
    vol = volume;
    vol = vol << 8;
    vol += v;

    switch(device) {
    case 1:
        res = ioctl(mixerfd, MIXER_WRITE(SOUND_MIXER_VOLUME), &vol);
        break;
    case 2:
        res = ioctl(mixerfd, MIXER_WRITE(SOUND_MIXER_LINE), &vol);
        break;
    case 4:
        res = ioctl(mixerfd, MIXER_WRITE(SOUND_MIXER_PCM), &vol);
        break;
    case 8:
        res = ioctl(mixerfd, MIXER_WRITE(SOUND_MIXER_CD), &vol);
    }

    if (res==-1) {
                /* An undefined mixer channel was accessed */
		unavail |= device;
    }
    GetVolume();
}

/* Draws slider tick marks */
void DrawTicks()
{
int tx;
int tw = sw/10;
    for (tx = height-sheight/2 ; tx>0 ; tx -= (height-sheight/2)/10) {
        XDrawLine(disp, win, tickgc, 1, tx, tw, tx);
        XDrawLine(disp, win, tickgc, sw-tw, tx, sw-1, tx);
    }
}

/* Draw text buttons for mixer selection */
void DrawButtons()
{
GC s1,s2,s3,s4;
int x=sw+4;
GC i1, i2;

	i1 = i2 = greygc;
    switch(device) {
    case 1:
        s1 = textagc;
        s2 = s3 = s4 = textbgc;
        break;
    case 2:
        s2 = textagc;
        s1 = s3 = s4 = textbgc;
        break;
    case 4:
        s3 = textagc;
        s1 = s2 = s4 = textbgc;
        break;
    case 8:
        s4 = textagc;
        s1 = s2 = s3 = textbgc;
    }

	if (indevice & SOUND_MASK_LINE)
		i1 = ingc;
	if (indevice & SOUND_MASK_CD)
		i2 = ingc;

    XDrawLine(disp, win, botshad, sw+1, 1, sw+1, height-2);
    XDrawLine(disp, win, topshad, sw+2, 1, sw+2, height-2);
	if (devmask & SOUND_MASK_VOLUME)
	    XDrawString(disp, win, s1, x, voly, VOLTEXT, strlen(VOLTEXT));
	if (devmask & SOUND_MASK_LINE)
 		XDrawString(disp, win, s2, x, liney, LINTEXT, strlen(LINTEXT));
	if (devmask & SOUND_MASK_PCM)
		XDrawString(disp, win, s3, x, dspy, DSPTEXT, strlen(DSPTEXT));
	if (devmask & SOUND_MASK_CD)
 		XDrawString(disp, win, s4, x, cdy, CDTEXT, strlen(CDTEXT));
	XDrawLine(disp, win, i1, x, liney+2, width-3, liney+2);
	XDrawLine(disp, win, i2, x, cdy+2, width-3, cdy+2);
}


