1 /*
2  * asmix is the AfterStep sound volume control knob for X Windows.
3  * Copyright (c) 1998 original author unknown
4  * Copyright (c) 1998-2004  Albert "Tigr" Dorofeev <albert@tigr.net>
5  * Copyright (c) 2000  John "wizgrav" Gravezas <wizgrav@netsmart.gr>
6  *
7  * This software is distributed under GPL. For details see LICENSE file.
8  */
9 
10 #include "config.h"
11 #include <stdio.h>
12 #include <unistd.h>
13 #include <stdlib.h>
14 #include <string.h>
15 
16 #include <X11/Xlib.h>
17 #include <X11/xpm.h>
18 #include <X11/extensions/shape.h>
19 #include <time.h>
20 #include <math.h>
21 #include <X11/Xatom.h>
22 #include <sys/errno.h>
23 #include <sys/ioctl.h>
24 #include <fcntl.h>
25 #ifdef HAVE_SYS_SOUNDCARD_H
26 #  include <sys/soundcard.h>
27 #endif
28 #ifdef HAVE_LINUX_SOUNDCARD_H
29 #  include <linux/soundcard.h>
30 #endif
31 
32 typedef struct mixer_channel_ident_t
33 {
34   char * channel_name;
35   int    channel_id;
36 } mixer_channel_ident;
37 
38 mixer_channel_ident channel_idents[] =
39   {
40     { "volume",    SOUND_MIXER_VOLUME	},
41     { "bass",      SOUND_MIXER_BASS	},
42     { "treble",    SOUND_MIXER_TREBLE	},
43     { "synth",     SOUND_MIXER_SYNTH	},
44     { "pcm",       SOUND_MIXER_PCM	},
45     { "speaker",   SOUND_MIXER_SPEAKER	},
46     { "line",      SOUND_MIXER_LINE	},
47     { "mic",       SOUND_MIXER_MIC	},
48     { "cd",        SOUND_MIXER_CD	},
49     { "imix",      SOUND_MIXER_IMIX	},
50     { "pcm2",      SOUND_MIXER_ALTPCM	},
51     { "record",    SOUND_MIXER_RECLEV	},
52     { "igain",     SOUND_MIXER_IGAIN	},
53     { "ogain",     SOUND_MIXER_OGAIN	},
54     { "line1",     SOUND_MIXER_LINE1	},
55     { "line2",     SOUND_MIXER_LINE2	},
56     { "line3",     SOUND_MIXER_LINE3	},
57     { "digital1",  SOUND_MIXER_DIGITAL1	},
58     { "digital2",  SOUND_MIXER_DIGITAL2	},
59     { "digital3",  SOUND_MIXER_DIGITAL3	},
60     { "phone-in",  SOUND_MIXER_PHONEIN	},
61     { "phone-out", SOUND_MIXER_PHONEOUT	},
62     { "video",     SOUND_MIXER_VIDEO	},
63     { "radio",     SOUND_MIXER_RADIO	},
64     { "monitor",   SOUND_MIXER_MONITOR	},
65     { NULL, 0 }
66   };
67 
68 typedef struct stereovolume
69 {
70   unsigned char left;
71   unsigned char right;
72 
73 } StereoVolume;
74 
75 
76 typedef struct volctrl
77 {
78   int mixer_id;
79   StereoVolume volume;
80   int supported;
81 } VolumeControl;
82 
83 VolumeControl Master;
84 int mixer_fd;
85 
86 #include "volume.xpm"
87 /*#include "mask2.xbm"*/
88 #include "mark.xpm"
89 
90 #define Center_X 24
91 #define Center_Y 26
92 #define Radius   13
93 #define min_a    0.75*M_PI
94 #define max_a    0.25*M_PI
95 #define WIZ     M_PI/8
96 
97 int ONLYSHAPE=0;
98 int ICONIFIED=0; /* default is not iconified */
99 int WITHDRAWN=0; /* default is not withdrawn */
100 
101 
102 /* X11 Variablen *************************************************************/
103 Display *dpy;	  /* welches DISPLAY */
104 Window Root;      /* Hintergrund-Drawable */
105 int screen;
106 
107 double Mark_Pos=M_PI/2;
108 int Pos=0;
109 
110 int d_depth;
111 XSizeHints mysizehints;
112 XWMHints mywmhints;
113 Pixel back_pix, fore_pix;
114 GC NormalGC;
115 Window iconwin, win;       /* My home is my window */
116 char *ProgName;
117 char *Geometry;
118 char Execute[] = "echo no program has been specified >/dev/console";
119 char *ERR_colorcells = "not enough free color cells\n";
120 
121 Atom wm_delete_window;
122 Atom wm_protocols;
123 
124 /* XPM Variablen *************************************************************/
125 typedef struct _XpmIcon {
126     Pixmap pixmap;
127     Pixmap mask;
128     XpmAttributes attributes;
129 }        XpmIcon;
130 
131 XpmIcon asMix,  Mark;
132 time_t actualtime;
133 
134 /* lokale Funktionen *********************************************************/
135 #define MW_EVENTS   (ExposureMask | ButtonPressMask | StructureNotifyMask | \
136                       ButtonMotionMask | PointerMotionMask)
137 #define FALSE 0
138 void GetXPM(void);
139 Pixel GetColor(char *name);
140 void RedrawWindow( XpmIcon *v);
141 
142 /*****************************************************************************/
143 /*****************************************************************************/
144 static char *help_message[] = {
145 "where options include:",
146 "    -exe <program>          program to start on click",
147 "    -geometry [+|-]x[+|-]y  position of asmix",
148 "    -shape                  without groundplate",
149 "    -iconic                 start up as icon",
150 "    -withdrawn              start up in withdrawn mode",
151 "    -channel <channel-id>   specify the channel to control (def: volume)",
152 "    -device <mixer-device>  specify the mixer device (def: /dev/mixer)",
153 "    -name <window-name>     specify the name of the window (def: asmix)",
154 "    -V                      version control",
155 NULL
156 };
157 
version()158 void version()
159 {
160 	printf("asmix: AfterStep sound mixer volume control 1.5\n");
161 }
162 
usage()163 void usage()
164 {
165   char **cpp;
166 
167   printf("usage:  %s [-options ...] \n", ProgName);
168   for (cpp = help_message; *cpp; cpp++) {
169     printf("%s\n", *cpp);
170   }
171   printf("\n");
172   exit(1);
173 }
174 
UpdatePos(double a,int flag)175 void UpdatePos(double a,int flag)
176 {
177    XCopyArea(dpy,asMix.pixmap,win,NormalGC,
178      Center_X+Radius*cos(Mark_Pos)-Mark.attributes.width/2,
179      Center_Y+Radius*sin(Mark_Pos)-Mark.attributes.height/2,
180      Mark.attributes.width, Mark.attributes.height,
181      Center_X+Radius*cos(Mark_Pos)-Mark.attributes.width/2,
182      Center_Y+Radius*sin(Mark_Pos)-Mark.attributes.height/2);
183    XCopyArea(dpy,asMix.pixmap,iconwin,NormalGC,
184      Center_X+Radius*cos(Mark_Pos)-Mark.attributes.width/2,
185      Center_Y+Radius*sin(Mark_Pos)-Mark.attributes.height/2,
186      Mark.attributes.width, Mark.attributes.height,
187      Center_X+Radius*cos(Mark_Pos)-Mark.attributes.width/2,
188      Center_Y+Radius*sin(Mark_Pos)-Mark.attributes.height/2);
189 
190    if(flag==1){Mark_Pos=a;flag=0;}
191       else Mark_Pos=a;
192 
193   Pos=Master.volume.left=Master.volume.right=(unsigned char)aToVol(Mark_Pos);
194 
195 /*   fprintf(stderr,"set_slider: updating mixer %i to %i:%i\n",Master.mixer_id,Master.volume.left,Master.volume.right);
196 */
197   if (ioctl(mixer_fd,MIXER_WRITE(Master.mixer_id),&Master.volume) == -1)
198         fprintf(stderr,"Error writing mixer in Handle_slider");
199 
200    XCopyArea(dpy,Mark.pixmap,win,NormalGC,
201      0,0,Mark.attributes.width, Mark.attributes.height,
202      Center_X+Radius*cos(Mark_Pos)-Mark.attributes.width/2,
203      Center_Y+Radius*sin(Mark_Pos)-Mark.attributes.height/2);
204    XCopyArea(dpy,Mark.pixmap,iconwin,NormalGC,
205      0,0,Mark.attributes.width, Mark.attributes.height,
206      Center_X+Radius*cos(Mark_Pos)-Mark.attributes.width/2,
207      Center_Y+Radius*sin(Mark_Pos)-Mark.attributes.height/2);
208 }
209 
aToVol(double a)210 int aToVol(double a)
211 {
212 
213   int retval;
214 
215   if ((a<0) || (a<M_PI/2)) a+=2*M_PI;
216 
217   a-=3*M_PI/4;
218 
219   retval=(int)(100*a/(1.5*M_PI));
220   if (retval<0) retval=0; else if (retval>100) retval=100;
221 
222   return retval;
223 }
224 
VolToa(int p)225 double VolToa(int p)
226 {
227   double NewPos;
228 
229   NewPos=(float)p/100*3*M_PI/2+3*M_PI/4;
230   if ((NewPos>M_PI)&&(NewPos<2*M_PI)) NewPos-=2*M_PI;
231 
232   return NewPos;
233 }
234 
MouseMove(int x,int y)235 void MouseMove(int x, int y)
236 {
237    double X,Y;
238    double a;
239 
240    X= (double)x - Center_X;
241    Y= (double)y - Center_Y;
242 
243    if (sqrt(X*X+Y*Y)<=2) return;
244 
245    a=atan2(Y,X);
246 
247    if ((a<min_a) &&(a>max_a)) {
248       if ((Mark_Pos<-M_PI/2)||(Mark_Pos>M_PI/2)) a=min_a; else a=max_a;
249    }
250 
251    UpdatePos(a,0);
252 }
253 
254 
sync_Control(VolumeControl * vcptr)255 static void sync_Control(VolumeControl *vcptr)
256 {
257   int portion;
258 
259   /*if (!vcptr->supported)
260     return;*/
261   if (ioctl(mixer_fd,MIXER_READ(vcptr->mixer_id),&vcptr->volume) == -1)
262     perror("Error reading volumes in sync_slider");
263 
264   portion=(vcptr->volume.left+vcptr->volume.right)/2;
265 
266 /*  fprintf(stderr,"%f : %i : %i\n",NewPos,Pos,portion);*/
267 
268   if (Pos!=portion) {
269      UpdatePos(VolToa(portion),0);
270      Pos=portion;
271   }
272 }
273 
274 
main(int argc,char * argv[])275 int main(int argc,char *argv[])
276 {
277   int i;
278   unsigned int borderwidth ;
279   char *display_name = NULL;
280   char *wname = "asmix";
281   char *mixer_device = "/dev/mixer";
282   XGCValues gcv;
283   unsigned long gcm;
284   XEvent Event;
285   XTextProperty name;
286   XClassHint classHint;
287   Pixmap pixmask;
288   int status;
289 
290   ProgName = argv[0];
291   Geometry = "";
292 
293 
294   Master.mixer_id  = SOUND_MIXER_VOLUME;
295   Master.supported = SOUND_MIXER_VOLUME;
296 
297   /* Parse command line options */
298   ProgName = argv[0];
299 
300   for(i=1;i<argc;i++) {
301     char *arg= argv[i];
302 
303     if (arg[0] == '-') {
304       switch(arg[1]) {
305       case 'e':
306 	if(++i >=argc) usage();
307 	strcpy(&Execute[0], argv[i]);
308 	strcat(&Execute[0], " &");
309 	continue;
310       case 's':
311 	ONLYSHAPE=1;
312 	continue;
313       case 'g':
314 	if(++i >=argc) usage();
315 	Geometry = argv[i];
316 	continue;
317       case 'i':
318 	ICONIFIED=1;
319 	continue;
320       case 'w':
321 	WITHDRAWN=1;
322 	continue;
323       case 'c':
324 	if(++i >=argc) usage();
325 	{
326 	  int cur=0;
327 	  while(1)
328 	  {
329 	    if ( channel_idents[cur].channel_name == NULL )
330 	      usage();
331 	    if ( strcmp( channel_idents[cur].channel_name, argv[i] ) != 0 )
332 	    {
333 	      cur++;
334 	      continue;
335 	    }
336 	    Master.mixer_id  = channel_idents[cur].channel_id;
337 	    Master.supported = channel_idents[cur].channel_id;
338 	    break;
339 	  }
340 	}
341 	continue;
342       case 'd':
343 	if(++i >=argc) usage();
344 	mixer_device=argv[i];
345 	continue;
346       case 'n':
347 	if(++i >=argc) usage();
348 	wname=argv[i];
349 	continue;
350       case 'V':
351 	version();
352 	exit(0);
353       default:
354 	version();
355 	usage();
356       }
357     }
358   }
359 
360   mixer_fd = open (mixer_device, O_RDWR, 0);
361   if (mixer_fd < 0) {
362     fprintf (stderr,"asmix: Error opening mixer device %s", mixer_device);
363     exit (1);
364   }
365 
366   if (ioctl(mixer_fd, SOUND_MIXER_READ_DEVMASK, &Master.supported) == -1)
367     Master.supported = 0xffff; /* Assume all are supported */
368 /*  printf("Master.supported = 0x%x\n",Master.supported);*/
369 
370 
371   /* Open the display */
372   if (!(dpy = XOpenDisplay(display_name)))
373     {
374       fprintf(stderr,"asmix: can't open display %s\n",
375 	      XDisplayName(display_name));
376       exit (1);
377     }
378   screen= DefaultScreen(dpy);
379   Root = RootWindow(dpy, screen);
380   d_depth = DefaultDepth(dpy, screen);
381 
382   /* Icon Daten nach XImage konvertieren */
383   GetXPM();
384 
385   /* Create a window to hold the banner */
386   mysizehints.flags= USSize|USPosition;
387   mysizehints.x = 0;
388   mysizehints.y = 0;
389 
390   back_pix = GetColor("grey");
391   fore_pix = GetColor("darkgrey");
392 
393   XWMGeometry(dpy, screen, Geometry, NULL, (borderwidth =1), &mysizehints,
394 	      &mysizehints.x,&mysizehints.y,&mysizehints.width,&mysizehints.height, &i);
395 
396   mysizehints.width = asMix.attributes.width;
397   mysizehints.height= asMix.attributes.height;
398 
399   win = XCreateSimpleWindow(dpy,Root,mysizehints.x,mysizehints.y,
400 			    mysizehints.width,mysizehints.height,
401 			    borderwidth,fore_pix,back_pix);
402   iconwin = XCreateSimpleWindow(dpy,win,mysizehints.x,mysizehints.y,
403 				mysizehints.width,mysizehints.height,
404 				borderwidth,fore_pix,back_pix);
405 
406 
407         /* Set up the event for quitting the window */
408         wm_delete_window = XInternAtom(
409                 dpy,
410                 "WM_DELETE_WINDOW",     /* atom_name */
411                 False                   /* only_if_exists */
412                 );
413         wm_protocols = XInternAtom(
414                 dpy,
415                 "WM_PROTOCOLS",         /* atom_name */
416                 False                   /* only_if_exists */
417                 );
418         status = XSetWMProtocols(
419                 dpy,
420                 win,
421                 &wm_delete_window,
422                 1
423                 );
424         status = XSetWMProtocols(
425                 dpy,
426                 iconwin,
427                 &wm_delete_window,
428                 1
429                 );
430 
431 
432   /* Hints aktivieren */
433   XSetWMNormalHints(dpy, win, &mysizehints);
434   classHint.res_name =  "asmix";
435   classHint.res_class = "asMix";
436   XSetClassHint(dpy, win, &classHint);
437 
438   XSelectInput(dpy, win, MW_EVENTS);
439   XSelectInput(dpy, iconwin, MW_EVENTS);
440 
441   if (XStringListToTextProperty(&wname, 1, &name) ==0) {
442     fprintf(stderr, "asmix: can't allocate window name [%s]\n", wname);
443     exit(-1);
444   }
445   XSetWMName(dpy, win, &name);
446   XSetWMName(dpy, iconwin, &name);
447 
448   /* Create a GC for drawing */
449   gcm = GCForeground|GCBackground|GCGraphicsExposures;
450   gcv.foreground = fore_pix;
451   gcv.background = back_pix;
452   gcv.graphics_exposures = FALSE;
453   NormalGC = XCreateGC(dpy, Root, gcm, &gcv);
454 
455   if (ONLYSHAPE) {
456 /* try to make shaped window here */
457 /*    pixmask = XCreateBitmapFromData(dpy, win, mask_bits,
458 				    mask_width, mask_height);*/
459     XShapeCombineMask(dpy, win, ShapeBounding, 0, 0, asMix.mask, ShapeSet);
460     XShapeCombineMask(dpy, iconwin, ShapeBounding, 0, 0, asMix.mask, ShapeSet);
461   }
462 
463   mywmhints.initial_state =
464 	  WITHDRAWN ? WithdrawnState :
465 	  ICONIFIED ? IconicState : NormalState;
466   mywmhints.window_group = win;
467   mywmhints.flags = StateHint | IconWindowHint | IconPositionHint | WindowGroupHint;
468   mywmhints.icon_window = iconwin;
469   mywmhints.icon_x = mysizehints.x;
470   mywmhints.icon_y = mysizehints.y;
471   XSetWMHints(dpy, win, &mywmhints);
472 
473   status = XSetCommand(dpy, win, argv, argc);
474 
475   XSetWindowBackgroundPixmap(dpy,win,asMix.pixmap);
476   XSetWindowBackgroundPixmap(dpy,iconwin,asMix.pixmap);
477 
478   sync_Control(&Master);
479 
480   XMapWindow(dpy,win);
481 
482   RedrawWindow(&asMix);
483   while(1)
484     {
485       if (actualtime != time(0))
486 	{
487 	  actualtime = time(0);
488 
489           sync_Control(&Master);
490 
491 	}
492 
493       /* read a packet */
494       while (XPending(dpy))
495 	{
496 	  XNextEvent(dpy,&Event);
497 	  switch(Event.type)
498 	    {
499                 case ClientMessage:
500                         if ((Event.xclient.message_type == wm_protocols)
501                           && (Event.xclient.data.l[0] == wm_delete_window))
502 			{
503 			      XCloseDisplay(dpy);
504 			      exit(0);
505 			}
506                         break;
507 	    case Expose:
508 	      if(Event.xexpose.count == 0 )
509 		RedrawWindow(&asMix);
510 	      break;
511 	    case ButtonPress:
512               if (Event.xbutton.button == Button1) {
513                   MouseMove(Event.xbutton.x, Event.xbutton.y);
514                } else if (Event.xbutton.button == Button2) {
515 	           system(Execute);
516 	       }
517                  else if (Event.xbutton.button == Button4) {
518 		   UpdatePos(WIZ,1);
519 	       }
520 	         else if (Event.xbutton.button == Button4) {
521 		   UpdatePos(-WIZ,1);
522 	       }
523 	      break;
524 	    case  MotionNotify: {
525 	        Window Root, Child;
526 	        int root_x, root_y;
527 	        int win_x, win_y;
528 	        unsigned int mask;
529 
530                 if (XQueryPointer(dpy, win, &Root, &Child,
531                     &root_x, &root_y, &win_x, &win_y, &mask)!=0) {
532                    if (mask & Button1MotionMask)
533                       MouseMove(Event.xbutton.x, Event.xbutton.y);
534                  }
535 	        break;
536 	      }
537 	    case DestroyNotify:
538               XFreeGC(dpy, NormalGC);
539       XFlush(dpy);
540 /*              fprintf(stderr,"DestroyMe?\n");*/
541 /*              XDestroyWindow(dpy, win);
542 	      XDestroyWindow(dpy, iconwin);*/
543               XCloseDisplay(dpy);
544 	      exit(0);
545 	    default:
546 	      break;
547 	    }
548 	}
549 #ifdef SYSV
550       poll((struct poll *) 0, (size_t) 0, 50);
551 #else
552       usleep(50000L);			/* 50/100 sec */
553 #endif
554     }
555   return 0;
556 }
557 /****************************************************************************/
nocolor(char * a,char * b)558 void nocolor(char *a, char *b)
559 {
560  fprintf(stderr,"asmix: can't %s %s\n", a,b);
561 }
562 /****************************************************************************/
563 /* Konvertiere XPMIcons nach XImage */
GetXPM(void)564 void GetXPM(void)
565 {
566   static char **bg_xpm;
567   XColor col;
568   XWindowAttributes attributes;
569   int ret;
570 
571   bg_xpm =ONLYSHAPE ? volume_xpm : volume_xpm;
572 
573   /* for the colormap */
574   XGetWindowAttributes(dpy,Root,&attributes);
575 
576   asMix.attributes.valuemask |= (XpmReturnPixels | XpmReturnExtensions);
577   ret = XpmCreatePixmapFromData(dpy, Root, bg_xpm, &asMix.pixmap,
578 				&asMix.mask, &asMix.attributes);
579   if(ret != XpmSuccess)
580     {fprintf(stderr, ERR_colorcells);exit(1);}
581 
582 
583   Mark.attributes.valuemask |= (XpmReturnPixels | XpmReturnExtensions);
584   ret = XpmCreatePixmapFromData(dpy, Root, mark_xpm, &Mark.pixmap,
585 				&Mark.mask, &Mark.attributes);
586   if(ret != XpmSuccess)
587     {fprintf(stderr, ERR_colorcells);exit(1);}
588 
589 }
590 /****************************************************************************/
591 /* Removes expose events for a specific window from the queue */
flush_expose(Window w)592 int flush_expose (Window w)
593 {
594   XEvent dummy;
595   int i=0;
596 
597   while (XCheckTypedWindowEvent (dpy, w, Expose, &dummy))i++;
598   return i;
599 }
600 
601 /****************************************************************************/
602 /* Draws the icon window */
RedrawWindow(XpmIcon * v)603 void RedrawWindow( XpmIcon *v)
604 {
605   flush_expose (iconwin);
606 
607     XCopyArea(dpy,Mark.pixmap,win,NormalGC,
608               0,0,Mark.attributes.width, Mark.attributes.height,
609               Center_X+Radius*cos(Mark_Pos)-Mark.attributes.width/2,
610               Center_Y+Radius*sin(Mark_Pos)-Mark.attributes.height/2);
611 
612    XCopyArea(dpy,Mark.pixmap,iconwin,NormalGC,
613               0,0,Mark.attributes.width, Mark.attributes.height,
614               Center_X+Radius*cos(Mark_Pos)-Mark.attributes.width/2,
615               Center_Y+Radius*sin(Mark_Pos)-Mark.attributes.height/2);
616 
617 }
618 /****************************************************************************/
GetColor(char * name)619 Pixel GetColor(char *name)
620 {
621   XColor color;
622   XWindowAttributes attributes;
623 
624   XGetWindowAttributes(dpy,Root,&attributes);
625   color.pixel = 0;
626    if (!XParseColor (dpy, attributes.colormap, name, &color))
627      {
628        nocolor("parse",name);
629      }
630    else if(!XAllocColor (dpy, attributes.colormap, &color))
631      {
632        nocolor("alloc",name);
633      }
634   return color.pixel;
635 }
636